use ratex_font::{get_char_metrics, get_global_metrics, FontId};
use ratex_parser::parse_node::{AtomFamily, Mode, ParseNode};
use ratex_types::color::Color;
use ratex_types::math_style::MathStyle;
use ratex_types::path_command::PathCommand;
use crate::hbox::make_hbox;
use crate::layout_box::{BoxContent, LayoutBox};
use crate::layout_options::LayoutOptions;
use crate::katex_svg::parse_svg_path_data;
use crate::spacing::{atom_spacing, mu_to_em, MathClass};
use crate::stacked_delim::make_stacked_delim_if_needed;
const NULL_DELIMITER_SPACE: f64 = 0.12;
pub fn layout(nodes: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
layout_expression(nodes, options, true)
}
fn apply_bin_cancellation(raw: &[Option<MathClass>]) -> Vec<Option<MathClass>> {
let n = raw.len();
let mut eff = raw.to_vec();
for i in 0..n {
if raw[i] != Some(MathClass::Bin) {
continue;
}
let prev = if i == 0 { None } else { raw[i - 1] };
let left_cancel = matches!(
prev,
None
| Some(MathClass::Bin)
| Some(MathClass::Open)
| Some(MathClass::Rel)
| Some(MathClass::Op)
| Some(MathClass::Punct)
);
if left_cancel {
eff[i] = Some(MathClass::Ord);
}
}
for i in 0..n {
if raw[i] != Some(MathClass::Bin) {
continue;
}
let next = if i + 1 < n { raw[i + 1] } else { None };
let right_cancel = matches!(
next,
None | Some(MathClass::Rel) | Some(MathClass::Close) | Some(MathClass::Punct)
);
if right_cancel {
eff[i] = Some(MathClass::Ord);
}
}
eff
}
fn node_is_middle_fence(node: &ParseNode) -> bool {
matches!(node, ParseNode::Middle { .. })
}
fn layout_expression(
nodes: &[ParseNode],
options: &LayoutOptions,
is_real_group: bool,
) -> LayoutBox {
if nodes.is_empty() {
return LayoutBox::new_empty();
}
let has_cr = nodes.iter().any(|n| matches!(n, ParseNode::Cr { .. }));
if has_cr {
return layout_multiline(nodes, options, is_real_group);
}
let raw_classes: Vec<Option<MathClass>> =
nodes.iter().map(node_math_class).collect();
let eff_classes = apply_bin_cancellation(&raw_classes);
let mut children = Vec::new();
let mut prev_class: Option<MathClass> = None;
let mut prev_class_node_idx: Option<usize> = None;
for (i, node) in nodes.iter().enumerate() {
let lbox = layout_node(node, options);
let cur_class = eff_classes.get(i).copied().flatten();
if is_real_group {
if let (Some(prev), Some(cur)) = (prev_class, cur_class) {
let prev_middle = prev_class_node_idx
.is_some_and(|j| node_is_middle_fence(&nodes[j]));
let cur_middle = node_is_middle_fence(node);
let mu = if prev_middle || cur_middle {
0.0
} else {
atom_spacing(prev, cur, options.style.is_tight())
};
let mu = if let Some(cap) = options.align_relation_spacing {
if prev == MathClass::Rel || cur == MathClass::Rel {
mu.min(cap)
} else {
mu
}
} else {
mu
};
if mu > 0.0 {
let em = mu_to_em(mu, options.metrics().quad);
children.push(LayoutBox::new_kern(em));
}
}
}
if cur_class.is_some() {
prev_class = cur_class;
prev_class_node_idx = Some(i);
}
children.push(lbox);
}
make_hbox(children)
}
fn layout_multiline(
nodes: &[ParseNode],
options: &LayoutOptions,
is_real_group: bool,
) -> LayoutBox {
use crate::layout_box::{BoxContent, VBoxChild, VBoxChildKind};
let metrics = options.metrics();
let pt = 1.0 / metrics.pt_per_em;
let baselineskip = 12.0 * pt; let lineskip = 1.0 * pt;
let mut rows: Vec<&[ParseNode]> = Vec::new();
let mut start = 0;
for (i, node) in nodes.iter().enumerate() {
if matches!(node, ParseNode::Cr { .. }) {
rows.push(&nodes[start..i]);
start = i + 1;
}
}
rows.push(&nodes[start..]);
let row_boxes: Vec<LayoutBox> = rows
.iter()
.map(|row| layout_expression(row, options, is_real_group))
.collect();
let total_width = row_boxes.iter().map(|b| b.width).fold(0.0_f64, f64::max);
let mut vchildren: Vec<VBoxChild> = Vec::new();
let mut h = row_boxes.first().map(|b| b.height).unwrap_or(0.0);
let d = row_boxes.last().map(|b| b.depth).unwrap_or(0.0);
for (i, row) in row_boxes.iter().enumerate() {
if i > 0 {
let prev_depth = row_boxes[i - 1].depth;
let gap = (baselineskip - prev_depth - row.height).max(lineskip);
vchildren.push(VBoxChild { kind: VBoxChildKind::Kern(gap), shift: 0.0 });
h += gap + row.height + prev_depth;
}
vchildren.push(VBoxChild {
kind: VBoxChildKind::Box(Box::new(row.clone())),
shift: 0.0,
});
}
LayoutBox {
width: total_width,
height: h,
depth: d,
content: BoxContent::VBox(vchildren),
color: options.color,
}
}
fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
match node {
ParseNode::MathOrd { text, mode, .. } => layout_symbol(text, *mode, options),
ParseNode::TextOrd { text, mode, .. } => layout_symbol(text, *mode, options),
ParseNode::Atom { text, mode, .. } => layout_symbol(text, *mode, options),
ParseNode::OpToken { text, mode, .. } => layout_symbol(text, *mode, options),
ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true),
ParseNode::SupSub {
base, sup, sub, ..
} => {
if let Some(base_node) = base.as_deref() {
if should_use_op_limits(base_node, options) {
return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
}
}
layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None)
}
ParseNode::GenFrac {
numer,
denom,
has_bar_line,
bar_size,
left_delim,
right_delim,
continued,
..
} => {
let bar_thickness = if *has_bar_line {
bar_size
.as_ref()
.map(|m| measurement_to_em(m, options))
.unwrap_or(options.metrics().default_rule_thickness)
} else {
0.0
};
let frac = layout_fraction(numer, denom, bar_thickness, *continued, options);
let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
if has_left || has_right {
let total_h = genfrac_delim_target_height(options);
let left_d = left_delim.as_deref().unwrap_or(".");
let right_d = right_delim.as_deref().unwrap_or(".");
let left_box = make_stretchy_delim(left_d, total_h, options);
let right_box = make_stretchy_delim(right_d, total_h, options);
let width = left_box.width + frac.width + right_box.width;
let height = frac.height.max(left_box.height).max(right_box.height);
let depth = frac.depth.max(left_box.depth).max(right_box.depth);
LayoutBox {
width,
height,
depth,
content: BoxContent::LeftRight {
left: Box::new(left_box),
right: Box::new(right_box),
inner: Box::new(frac),
},
color: options.color,
}
} else {
let right_nds = if *continued { 0.0 } else { NULL_DELIMITER_SPACE };
make_hbox(vec![
LayoutBox::new_kern(NULL_DELIMITER_SPACE),
frac,
LayoutBox::new_kern(right_nds),
])
}
}
ParseNode::Sqrt { body, index, .. } => {
layout_radical(body, index.as_deref(), options)
}
ParseNode::Op {
name,
symbol,
body,
limits,
suppress_base_shift,
..
} => layout_op(
name.as_deref(),
*symbol,
body.as_deref(),
*limits,
suppress_base_shift.unwrap_or(false),
options,
),
ParseNode::OperatorName { body, .. } => layout_operatorname(body, options),
ParseNode::SpacingNode { text, .. } => layout_spacing_command(text, options),
ParseNode::Kern { dimension, .. } => {
let em = measurement_to_em(dimension, options);
LayoutBox::new_kern(em)
}
ParseNode::Color { color, body, .. } => {
let new_color = Color::parse(color).unwrap_or(options.color);
let new_opts = options.with_color(new_color);
let mut lbox = layout_expression(body, &new_opts, true);
lbox.color = new_color;
lbox
}
ParseNode::Styling { style, body, .. } => {
let new_style = match style {
ratex_parser::parse_node::StyleStr::Display => MathStyle::Display,
ratex_parser::parse_node::StyleStr::Text => MathStyle::Text,
ratex_parser::parse_node::StyleStr::Script => MathStyle::Script,
ratex_parser::parse_node::StyleStr::Scriptscript => MathStyle::ScriptScript,
};
let ratio = new_style.size_multiplier() / options.style.size_multiplier();
let new_opts = options.with_style(new_style);
let inner = layout_expression(body, &new_opts, true);
if (ratio - 1.0).abs() < 0.001 {
inner
} else {
LayoutBox {
width: inner.width * ratio,
height: inner.height * ratio,
depth: inner.depth * ratio,
content: BoxContent::Scaled {
body: Box::new(inner),
child_scale: ratio,
},
color: options.color,
}
}
}
ParseNode::Accent {
label, base, is_stretchy, is_shifty, ..
} => {
let is_below = matches!(label.as_str(), "\\c");
layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options)
}
ParseNode::AccentUnder {
label, base, is_stretchy, ..
} => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options),
ParseNode::LeftRight {
body, left, right, ..
} => layout_left_right(body, left, right, options),
ParseNode::DelimSizing {
size, delim, ..
} => layout_delim_sizing(*size, delim, options),
ParseNode::Array {
body,
cols,
arraystretch,
add_jot,
row_gaps,
hlines_before_row,
col_separation_type,
hskip_before_and_after,
is_cd,
..
} => {
if is_cd.unwrap_or(false) {
layout_cd(body, options)
} else {
layout_array(
body,
cols.as_deref(),
*arraystretch,
add_jot.unwrap_or(false),
row_gaps,
hlines_before_row,
col_separation_type.as_deref(),
hskip_before_and_after.unwrap_or(false),
options,
)
}
}
ParseNode::CdArrow {
direction,
label_above,
label_below,
..
} => layout_cd_arrow(direction, label_above.as_deref(), label_below.as_deref(), 0.0, 0.0, 0.0, options),
ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options),
ParseNode::Text { body, font, mode, .. } => match font.as_deref() {
Some(f) => {
let group = ParseNode::OrdGroup {
mode: *mode,
body: body.clone(),
semisimple: None,
loc: None,
};
layout_font(f, &group, options)
}
None => layout_text(body, options),
},
ParseNode::Font { font, body, .. } => layout_font(font, body, options),
ParseNode::Href { body, .. } => layout_href(body, options),
ParseNode::Overline { body, .. } => layout_overline(body, options),
ParseNode::Underline { body, .. } => layout_underline(body, options),
ParseNode::Rule {
width: w,
height: h,
shift,
..
} => {
let width = measurement_to_em(w, options);
let ink_h = measurement_to_em(h, options);
let raise = shift
.as_ref()
.map(|s| measurement_to_em(s, options))
.unwrap_or(0.0);
let box_height = (raise + ink_h).max(0.0);
let box_depth = (-raise).max(0.0);
LayoutBox::new_rule(width, box_height, box_depth, ink_h, raise)
}
ParseNode::Phantom { body, .. } => {
let inner = layout_expression(body, options, true);
LayoutBox {
width: inner.width,
height: inner.height,
depth: inner.depth,
content: BoxContent::Empty,
color: Color::BLACK,
}
}
ParseNode::VPhantom { body, .. } => {
let inner = layout_node(body, options);
LayoutBox {
width: 0.0,
height: inner.height,
depth: inner.depth,
content: BoxContent::Empty,
color: Color::BLACK,
}
}
ParseNode::Smash { body, smash_height, smash_depth, .. } => {
let mut inner = layout_node(body, options);
if *smash_height { inner.height = 0.0; }
if *smash_depth { inner.depth = 0.0; }
inner
}
ParseNode::Middle { delim, .. } => {
match options.leftright_delim_height {
Some(h) => make_stretchy_delim(delim, h, options),
None => {
let placeholder = make_stretchy_delim(delim, 1.0, options);
LayoutBox {
width: placeholder.width,
height: 0.0,
depth: 0.0,
content: BoxContent::Empty,
color: options.color,
}
}
}
}
ParseNode::HtmlMathMl { html, .. } => {
layout_expression(html, options, true)
}
ParseNode::MClass { body, .. } => layout_expression(body, options, true),
ParseNode::MathChoice {
display, text, script, scriptscript, ..
} => {
let branch = match options.style {
MathStyle::Display | MathStyle::DisplayCramped => display,
MathStyle::Text | MathStyle::TextCramped => text,
MathStyle::Script | MathStyle::ScriptCramped => script,
MathStyle::ScriptScript | MathStyle::ScriptScriptCramped => scriptscript,
};
layout_expression(branch, options, true)
}
ParseNode::Lap { alignment, body, .. } => {
let inner = layout_node(body, options);
let shift = match alignment.as_str() {
"llap" => -inner.width,
"clap" => -inner.width / 2.0,
_ => 0.0, };
let mut children = Vec::new();
if shift != 0.0 {
children.push(LayoutBox::new_kern(shift));
}
let h = inner.height;
let d = inner.depth;
children.push(inner);
LayoutBox {
width: 0.0,
height: h,
depth: d,
content: BoxContent::HBox(children),
color: options.color,
}
}
ParseNode::HorizBrace {
base,
is_over,
label,
..
} => layout_horiz_brace(base, *is_over, label, options),
ParseNode::XArrow {
label, body, below, ..
} => layout_xarrow(label, body, below.as_deref(), options),
ParseNode::Pmb { body, .. } => layout_pmb(body, options),
ParseNode::HBox { body, .. } => layout_text(body, options),
ParseNode::Enclose { label, background_color, border_color, body, .. } => {
layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options)
}
ParseNode::RaiseBox { dy, body, .. } => {
let shift = measurement_to_em(dy, options);
layout_raisebox(shift, body, options)
}
ParseNode::VCenter { body, .. } => {
let inner = layout_node(body, options);
let axis = options.metrics().axis_height;
let total = inner.height + inner.depth;
let height = total / 2.0 + axis;
let depth = total - height;
LayoutBox {
width: inner.width,
height,
depth,
content: inner.content,
color: inner.color,
}
}
ParseNode::Verb { body, star, .. } => layout_verb(body, *star, options),
_ => LayoutBox::new_empty(),
}
}
fn missing_glyph_width_em(ch: char) -> f64 {
match ch as u32 {
0x3040..=0x30FF | 0x31F0..=0x31FF => 1.0,
0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF => 1.0,
0xAC00..=0xD7AF => 1.0,
0xFF01..=0xFF60 | 0xFFE0..=0xFFEE => 1.0,
_ => 0.5,
}
}
fn missing_glyph_metrics_fallback(ch: char, options: &LayoutOptions) -> (f64, f64, f64) {
let m = get_global_metrics(options.style.size_index());
let w = missing_glyph_width_em(ch);
if w >= 0.99 {
let h = (m.quad * 0.92).max(m.x_height);
(w, h, 0.0)
} else {
(w, m.x_height, 0.0)
}
}
#[inline]
fn math_glyph_advance_em(m: &ratex_font::CharMetrics, mode: Mode) -> f64 {
if mode == Mode::Math {
m.width + m.italic
} else {
m.width
}
}
fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox {
let ch = resolve_symbol_char(text, mode);
match ch as u32 {
0x22B7 => return layout_imageof_origof(true, options), 0x22B6 => return layout_imageof_origof(false, options), _ => {}
}
let char_code = ch as u32;
if let Some((font_id, metric_cp)) =
ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
{
let m = get_char_metrics(font_id, metric_cp);
let (width, height, depth) = match m {
Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
None => missing_glyph_metrics_fallback(ch, options),
};
return LayoutBox {
width,
height,
depth,
content: BoxContent::Glyph {
font_id,
char_code,
},
color: options.color,
};
}
let mut font_id = select_font(text, ch, mode, options);
let mut metrics = get_char_metrics(font_id, char_code);
if metrics.is_none() && mode == Mode::Math && font_id != FontId::MathItalic {
if let Some(m) = get_char_metrics(FontId::MathItalic, char_code) {
font_id = FontId::MathItalic;
metrics = Some(m);
}
}
let (width, height, depth) = match metrics {
Some(m) => (math_glyph_advance_em(&m, mode), m.height, m.depth),
None => missing_glyph_metrics_fallback(ch, options),
};
LayoutBox {
width,
height,
depth,
content: BoxContent::Glyph {
font_id,
char_code,
},
color: options.color,
}
}
fn resolve_symbol_char(text: &str, mode: Mode) -> char {
let font_mode = match mode {
Mode::Math => ratex_font::Mode::Math,
Mode::Text => ratex_font::Mode::Text,
};
if let Some(raw) = text.chars().next() {
let ru = raw as u32;
if (0x1D400..=0x1D7FF).contains(&ru) {
return raw;
}
}
if let Some(info) = ratex_font::get_symbol(text, font_mode) {
if let Some(cp) = info.codepoint {
return cp;
}
}
text.chars().next().unwrap_or('?')
}
fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOptions) -> FontId {
let font_mode = match mode {
Mode::Math => ratex_font::Mode::Math,
Mode::Text => ratex_font::Mode::Text,
};
if let Some(info) = ratex_font::get_symbol(text, font_mode) {
if info.font == ratex_font::SymbolFont::Ams {
return FontId::AmsRegular;
}
}
match mode {
Mode::Math => {
if resolved_char.is_ascii_lowercase()
|| resolved_char.is_ascii_uppercase()
|| is_math_italic_greek(resolved_char)
{
FontId::MathItalic
} else {
FontId::MainRegular
}
}
Mode::Text => FontId::MainRegular,
}
}
fn is_math_italic_greek(ch: char) -> bool {
matches!(ch,
'\u{03B1}'..='\u{03C9}' |
'\u{03D1}' | '\u{03D5}' | '\u{03D6}' |
'\u{03F1}' | '\u{03F5}'
)
}
fn is_arrow_accent(label: &str) -> bool {
matches!(
label,
"\\overrightarrow"
| "\\overleftarrow"
| "\\Overrightarrow"
| "\\overleftrightarrow"
| "\\underrightarrow"
| "\\underleftarrow"
| "\\underleftrightarrow"
| "\\overleftharpoon"
| "\\overrightharpoon"
| "\\overlinesegment"
| "\\underlinesegment"
)
}
fn layout_fraction(
numer: &ParseNode,
denom: &ParseNode,
bar_thickness: f64,
continued: bool,
options: &LayoutOptions,
) -> LayoutBox {
let numer_s = options.style.numerator();
let denom_s = options.style.denominator();
let numer_style = options.with_style(numer_s);
let denom_style = options.with_style(denom_s);
let mut numer_box = layout_node(numer, &numer_style);
if continued {
let pt = options.metrics().pt_per_em;
let h_min = 8.5 / pt;
let d_min = 3.5 / pt;
if numer_box.height < h_min {
numer_box.height = h_min;
}
if numer_box.depth < d_min {
numer_box.depth = d_min;
}
}
let denom_box = layout_node(denom, &denom_style);
let numer_ratio = numer_s.size_multiplier() / options.style.size_multiplier();
let denom_ratio = denom_s.size_multiplier() / options.style.size_multiplier();
let numer_height = numer_box.height * numer_ratio;
let numer_depth = numer_box.depth * numer_ratio;
let denom_height = denom_box.height * denom_ratio;
let denom_depth = denom_box.depth * denom_ratio;
let numer_width = numer_box.width * numer_ratio;
let denom_width = denom_box.width * denom_ratio;
let metrics = options.metrics();
let axis = metrics.axis_height;
let rule = bar_thickness;
let (mut num_shift, mut den_shift) = if options.style.is_display() {
(metrics.num1, metrics.denom1)
} else if bar_thickness > 0.0 {
(metrics.num2, metrics.denom2)
} else {
(metrics.num3, metrics.denom2)
};
if bar_thickness > 0.0 {
let min_clearance = if options.style.is_display() {
3.0 * rule
} else {
rule
};
let num_clearance = (num_shift - numer_depth) - (axis + rule / 2.0);
if num_clearance < min_clearance {
num_shift += min_clearance - num_clearance;
}
let den_clearance = (axis - rule / 2.0) + (den_shift - denom_height);
if den_clearance < min_clearance {
den_shift += min_clearance - den_clearance;
}
} else {
let min_gap = if options.style.is_display() {
7.0 * metrics.default_rule_thickness
} else {
3.0 * metrics.default_rule_thickness
};
let gap = (num_shift - numer_depth) - (denom_height - den_shift);
if gap < min_gap {
let adjust = (min_gap - gap) / 2.0;
num_shift += adjust;
den_shift += adjust;
}
}
let total_width = numer_width.max(denom_width);
let height = numer_height + num_shift;
let depth = denom_depth + den_shift;
LayoutBox {
width: total_width,
height,
depth,
content: BoxContent::Fraction {
numer: Box::new(numer_box),
denom: Box::new(denom_box),
numer_shift: num_shift,
denom_shift: den_shift,
bar_thickness: rule,
numer_scale: numer_ratio,
denom_scale: denom_ratio,
},
color: options.color,
}
}
fn layout_supsub(
base: Option<&ParseNode>,
sup: Option<&ParseNode>,
sub: Option<&ParseNode>,
options: &LayoutOptions,
inherited_font: Option<FontId>,
) -> LayoutBox {
let layout_child = |n: &ParseNode, opts: &LayoutOptions| match inherited_font {
Some(fid) => layout_with_font(n, fid, opts),
None => layout_node(n, opts),
};
let horiz_brace_over = matches!(
base,
Some(ParseNode::HorizBrace {
is_over: true,
..
})
);
let horiz_brace_under = matches!(
base,
Some(ParseNode::HorizBrace {
is_over: false,
..
})
);
let center_scripts = horiz_brace_over || horiz_brace_under;
let base_box = base
.map(|b| layout_child(b, options))
.unwrap_or_else(LayoutBox::new_empty);
let is_char_box = base.is_some_and(is_character_box);
let metrics = options.metrics();
let script_space = 0.5 / metrics.pt_per_em / options.size_multiplier();
let sup_style = options.style.superscript();
let sub_style = options.style.subscript();
let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
let sup_box = sup.map(|s| {
let sup_opts = options.with_style(sup_style);
layout_child(s, &sup_opts)
});
let sub_box = sub.map(|s| {
let sub_opts = options.with_style(sub_style);
layout_child(s, &sub_opts)
});
let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
let sup_style_metrics = get_global_metrics(sup_style.size_index());
let sub_style_metrics = get_global_metrics(sub_style.size_index());
let mut sup_shift = if !is_char_box && sup_box.is_some() {
base_box.height - sup_style_metrics.sup_drop * sup_ratio
} else {
0.0
};
let mut sub_shift = if !is_char_box && sub_box.is_some() {
base_box.depth + sub_style_metrics.sub_drop * sub_ratio
} else {
0.0
};
let min_sup_shift = if options.style.is_cramped() {
metrics.sup3
} else if options.style.is_display() {
metrics.sup1
} else {
metrics.sup2
};
if sup_box.is_some() && sub_box.is_some() {
sup_shift = sup_shift
.max(min_sup_shift)
.max(sup_depth_scaled + 0.25 * metrics.x_height);
sub_shift = sub_shift.max(metrics.sub2);
let rule_width = metrics.default_rule_thickness;
let max_width = 4.0 * rule_width;
let gap = (sup_shift - sup_depth_scaled) - (sub_height_scaled - sub_shift);
if gap < max_width {
sub_shift = max_width - (sup_shift - sup_depth_scaled) + sub_height_scaled;
let psi = 0.8 * metrics.x_height - (sup_shift - sup_depth_scaled);
if psi > 0.0 {
sup_shift += psi;
sub_shift -= psi;
}
}
} else if sub_box.is_some() {
sub_shift = sub_shift
.max(metrics.sub1)
.max(sub_height_scaled - 0.8 * metrics.x_height);
} else if sup_box.is_some() {
sup_shift = sup_shift
.max(min_sup_shift)
.max(sup_depth_scaled + 0.25 * metrics.x_height);
}
if horiz_brace_over && sup_box.is_some() {
sup_shift += sup_style_metrics.sup_drop * sup_ratio;
sup_shift += metrics.big_op_spacing1;
}
if horiz_brace_under && sub_box.is_some() {
sub_shift += sub_style_metrics.sub_drop * sub_ratio;
sub_shift += metrics.big_op_spacing2 + 0.2;
}
let italic_correction = 0.0;
let sub_h_kern = if sub_box.is_some() && !center_scripts {
-glyph_italic(&base_box)
} else {
0.0
};
let mut height = base_box.height;
let mut depth = base_box.depth;
let mut total_width = base_box.width;
if let Some(ref sup_b) = sup_box {
height = height.max(sup_shift + sup_height_scaled);
if center_scripts {
total_width = total_width.max(sup_b.width * sup_ratio + script_space);
} else {
total_width = total_width.max(
base_box.width + italic_correction + sup_b.width * sup_ratio + script_space,
);
}
}
if let Some(ref sub_b) = sub_box {
depth = depth.max(sub_shift + sub_depth_scaled);
if center_scripts {
total_width = total_width.max(sub_b.width * sub_ratio + script_space);
} else {
total_width = total_width.max(
base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space,
);
}
}
LayoutBox {
width: total_width,
height,
depth,
content: BoxContent::SupSub {
base: Box::new(base_box),
sup: sup_box.map(Box::new),
sub: sub_box.map(Box::new),
sup_shift,
sub_shift,
sup_scale: sup_ratio,
sub_scale: sub_ratio,
center_scripts,
italic_correction,
sub_h_kern,
},
color: options.color,
}
}
fn layout_radical(
body: &ParseNode,
index: Option<&ParseNode>,
options: &LayoutOptions,
) -> LayoutBox {
let cramped = options.style.cramped();
let cramped_opts = options.with_style(cramped);
let mut body_box = layout_node(body, &cramped_opts);
let body_ratio = cramped.size_multiplier() / options.style.size_multiplier();
body_box.height *= body_ratio;
body_box.depth *= body_ratio;
body_box.width *= body_ratio;
if body_box.height == 0.0 {
body_box.height = options.metrics().x_height;
}
let metrics = options.metrics();
let theta = metrics.default_rule_thickness;
let phi = if options.style.is_display() {
metrics.x_height
} else {
theta
};
let mut line_clearance = theta + phi / 4.0;
let min_delim_height = body_box.height + body_box.depth + line_clearance + theta;
let tex_height = select_surd_height(min_delim_height);
let rule_width = theta;
let surd_font = crate::surd::surd_font_for_inner_height(tex_height);
let advance_width = ratex_font::get_char_metrics(surd_font, 0x221A)
.map(|m| m.width)
.unwrap_or(0.833);
let delim_depth = tex_height - rule_width;
if delim_depth > body_box.height + body_box.depth + line_clearance {
line_clearance =
(line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0;
}
let img_shift = tex_height - body_box.height - line_clearance - rule_width;
let height = tex_height + rule_width - img_shift;
let depth = if img_shift > body_box.depth {
img_shift
} else {
body_box.depth
};
const INDEX_KERN: f64 = 0.05;
let (index_box, index_offset, index_scale) = if let Some(index_node) = index {
let root_style = options.style.superscript().superscript();
let root_opts = options.with_style(root_style);
let idx = layout_node(index_node, &root_opts);
let index_ratio = root_style.size_multiplier() / options.style.size_multiplier();
let offset = idx.width * index_ratio + INDEX_KERN;
(Some(Box::new(idx)), offset, index_ratio)
} else {
(None, 0.0, 1.0)
};
let width = index_offset + advance_width + body_box.width;
LayoutBox {
width,
height,
depth,
content: BoxContent::Radical {
body: Box::new(body_box),
index: index_box,
index_offset,
index_scale,
rule_thickness: rule_width,
inner_height: tex_height,
},
color: options.color,
}
}
fn select_surd_height(min_height: f64) -> f64 {
const SURD_HEIGHTS: [f64; 5] = [1.0, 1.2, 1.8, 2.4, 3.0];
for &h in &SURD_HEIGHTS {
if h >= min_height {
return h;
}
}
SURD_HEIGHTS[4].max(min_height)
}
const NO_SUCCESSOR: &[&str] = &["\\smallint"];
fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool {
match base {
ParseNode::Op {
limits,
always_handle_sup_sub,
..
} => {
*limits
&& (options.style.is_display()
|| always_handle_sup_sub.unwrap_or(false))
}
ParseNode::OperatorName {
always_handle_sup_sub,
limits,
..
} => {
*always_handle_sup_sub
&& (options.style.is_display() || *limits)
}
_ => false,
}
}
fn layout_op(
name: Option<&str>,
symbol: bool,
body: Option<&[ParseNode]>,
_limits: bool,
suppress_base_shift: bool,
options: &LayoutOptions,
) -> LayoutBox {
let (mut base_box, _slant) = build_op_base(name, symbol, body, options);
if symbol && !suppress_base_shift {
let axis = options.metrics().axis_height;
let shift = (base_box.height - base_box.depth) / 2.0 - axis;
if shift.abs() > 0.001 {
base_box.height -= shift;
base_box.depth += shift;
}
}
if !suppress_base_shift && !symbol && body.is_some() {
let axis = options.metrics().axis_height;
let delta = (base_box.height - base_box.depth) / 2.0 - axis;
if delta.abs() > 0.001 {
let w = base_box.width;
let raise = -delta;
base_box = LayoutBox {
width: w,
height: (base_box.height + raise).max(0.0),
depth: (base_box.depth - raise).max(0.0),
content: BoxContent::RaiseBox {
body: Box::new(base_box),
shift: raise,
},
color: options.color,
};
}
}
base_box
}
fn build_op_base(
name: Option<&str>,
symbol: bool,
body: Option<&[ParseNode]>,
options: &LayoutOptions,
) -> (LayoutBox, f64) {
if symbol {
let large = options.style.is_display()
&& !NO_SUCCESSOR.contains(&name.unwrap_or(""));
let font_id = if large {
FontId::Size2Regular
} else {
FontId::Size1Regular
};
let op_name = name.unwrap_or("");
let ch = resolve_op_char(op_name);
let char_code = ch as u32;
let metrics = get_char_metrics(font_id, char_code);
let (width, height, depth, italic) = match metrics {
Some(m) => (m.width, m.height, m.depth, m.italic),
None => (1.0, 0.75, 0.25, 0.0),
};
let width_with_italic = width + italic;
let base = LayoutBox {
width: width_with_italic,
height,
depth,
content: BoxContent::Glyph {
font_id,
char_code,
},
color: options.color,
};
if op_name == "\\oiint" || op_name == "\\oiiint" {
let w = base.width;
let ellipse_commands = ellipse_overlay_path(w, base.height, base.depth);
let overlay_box = LayoutBox {
width: w,
height: base.height,
depth: base.depth,
content: BoxContent::SvgPath {
commands: ellipse_commands,
fill: false,
},
color: options.color,
};
let with_overlay = make_hbox(vec![base, LayoutBox::new_kern(-w), overlay_box]);
return (with_overlay, italic);
}
(base, italic)
} else if let Some(body_nodes) = body {
let base = layout_expression(body_nodes, options, true);
(base, 0.0)
} else {
let base = layout_op_text(name.unwrap_or(""), options);
(base, 0.0)
}
}
fn layout_op_text(name: &str, options: &LayoutOptions) -> LayoutBox {
let text = name.strip_prefix('\\').unwrap_or(name);
let mut children = Vec::new();
for ch in text.chars() {
let char_code = ch as u32;
let metrics = get_char_metrics(FontId::MainRegular, char_code);
let (width, height, depth) = match metrics {
Some(m) => (m.width, m.height, m.depth),
None => (0.5, 0.43, 0.0),
};
children.push(LayoutBox {
width,
height,
depth,
content: BoxContent::Glyph {
font_id: FontId::MainRegular,
char_code,
},
color: options.color,
});
}
make_hbox(children)
}
fn compute_op_base_shift(base: &LayoutBox, options: &LayoutOptions) -> f64 {
let metrics = options.metrics();
(base.height - base.depth) / 2.0 - metrics.axis_height
}
fn resolve_op_char(name: &str) -> char {
match name {
"\\oiint" => return '\u{222C}', "\\oiiint" => return '\u{222D}', _ => {}
}
let font_mode = ratex_font::Mode::Math;
if let Some(info) = ratex_font::get_symbol(name, font_mode) {
if let Some(cp) = info.codepoint {
return cp;
}
}
name.chars().next().unwrap_or('?')
}
fn layout_op_with_limits(
base_node: &ParseNode,
sup_node: Option<&ParseNode>,
sub_node: Option<&ParseNode>,
options: &LayoutOptions,
) -> LayoutBox {
let (name, symbol, body, suppress_base_shift) = match base_node {
ParseNode::Op {
name,
symbol,
body,
suppress_base_shift,
..
} => (
name.as_deref(),
*symbol,
body.as_deref(),
suppress_base_shift.unwrap_or(false),
),
ParseNode::OperatorName { body, .. } => (None, false, Some(body.as_slice()), false),
_ => return layout_supsub(Some(base_node), sup_node, sub_node, options, None),
};
let (base_box, slant) = build_op_base(name, symbol, body, options);
let base_shift = if symbol && !suppress_base_shift {
compute_op_base_shift(&base_box, options)
} else {
0.0
};
layout_op_limits_inner(&base_box, sup_node, sub_node, slant, base_shift, options)
}
fn layout_op_limits_inner(
base: &LayoutBox,
sup_node: Option<&ParseNode>,
sub_node: Option<&ParseNode>,
slant: f64,
base_shift: f64,
options: &LayoutOptions,
) -> LayoutBox {
let metrics = options.metrics();
let sup_style = options.style.superscript();
let sub_style = options.style.subscript();
let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
let extra_clearance = 0.08_f64;
let sup_data = sup_node.map(|s| {
let sup_opts = options.with_style(sup_style);
let elem = layout_node(s, &sup_opts);
let kern = (metrics.big_op_spacing1 + extra_clearance)
.max(metrics.big_op_spacing3 - elem.depth * sup_ratio + extra_clearance);
(elem, kern)
});
let sub_data = sub_node.map(|s| {
let sub_opts = options.with_style(sub_style);
let elem = layout_node(s, &sub_opts);
let kern = (metrics.big_op_spacing2 + extra_clearance)
.max(metrics.big_op_spacing4 - elem.height * sub_ratio + extra_clearance);
(elem, kern)
});
let sp5 = metrics.big_op_spacing5;
let (total_height, total_depth, total_width) = match (&sup_data, &sub_data) {
(Some((sup_elem, sup_kern)), Some((sub_elem, sub_kern))) => {
let sup_h = sup_elem.height * sup_ratio;
let sup_d = sup_elem.depth * sup_ratio;
let sub_h = sub_elem.height * sub_ratio;
let sub_d = sub_elem.depth * sub_ratio;
let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift;
let height = bottom
+ base.height - base_shift
+ sup_kern
+ sup_h + sup_d
+ sp5
- (base.height + base.depth);
let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
let total_d = bottom;
let w = base
.width
.max(sup_elem.width * sup_ratio)
.max(sub_elem.width * sub_ratio);
let _ = height; (total_h, total_d, w)
}
(None, Some((sub_elem, sub_kern))) => {
let sub_h = sub_elem.height * sub_ratio;
let sub_d = sub_elem.depth * sub_ratio;
let total_h = base.height - base_shift;
let total_d = base.depth + base_shift + sub_kern + sub_h + sub_d + sp5;
let w = base.width.max(sub_elem.width * sub_ratio);
(total_h, total_d, w)
}
(Some((sup_elem, sup_kern)), None) => {
let sup_h = sup_elem.height * sup_ratio;
let sup_d = sup_elem.depth * sup_ratio;
let total_h =
base.height - base_shift + sup_kern + sup_h + sup_d + sp5;
let total_d = base.depth + base_shift;
let w = base.width.max(sup_elem.width * sup_ratio);
(total_h, total_d, w)
}
(None, None) => {
return base.clone();
}
};
let sup_kern_val = sup_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
let sub_kern_val = sub_data.as_ref().map(|(_, k)| *k).unwrap_or(0.0);
LayoutBox {
width: total_width,
height: total_height,
depth: total_depth,
content: BoxContent::OpLimits {
base: Box::new(base.clone()),
sup: sup_data.map(|(elem, _)| Box::new(elem)),
sub: sub_data.map(|(elem, _)| Box::new(elem)),
base_shift,
sup_kern: sup_kern_val,
sub_kern: sub_kern_val,
slant,
sup_scale: sup_ratio,
sub_scale: sub_ratio,
},
color: options.color,
}
}
fn layout_operatorname(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
let mut children = Vec::new();
for node in body {
match node {
ParseNode::MathOrd { text, .. } | ParseNode::TextOrd { text, .. } => {
let ch = text.chars().next().unwrap_or('?');
let char_code = ch as u32;
let metrics = get_char_metrics(FontId::MainRegular, char_code);
let (width, height, depth) = match metrics {
Some(m) => (m.width, m.height, m.depth),
None => (0.5, 0.43, 0.0),
};
children.push(LayoutBox {
width,
height,
depth,
content: BoxContent::Glyph {
font_id: FontId::MainRegular,
char_code,
},
color: options.color,
});
}
_ => {
children.push(layout_node(node, options));
}
}
}
make_hbox(children)
}
const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018;
fn glyph_italic(lb: &LayoutBox) -> f64 {
match &lb.content {
BoxContent::Glyph { font_id, char_code } => {
get_char_metrics(*font_id, *char_code)
.map(|m| m.italic)
.unwrap_or(0.0)
}
BoxContent::HBox(children) => {
children.last().map(glyph_italic).unwrap_or(0.0)
}
_ => 0.0,
}
}
fn accent_ordgroup_len(base: &ParseNode) -> usize {
match base {
ParseNode::OrdGroup { body, .. } => body.len().max(1),
_ => 1,
}
}
fn glyph_skew(lb: &LayoutBox) -> f64 {
match &lb.content {
BoxContent::Glyph { font_id, char_code } => {
get_char_metrics(*font_id, *char_code)
.map(|m| m.skew)
.unwrap_or(0.0)
}
BoxContent::HBox(children) => {
children.last().map(glyph_skew).unwrap_or(0.0)
}
_ => 0.0,
}
}
fn layout_accent(
label: &str,
base: &ParseNode,
is_stretchy: bool,
is_shifty: bool,
is_below: bool,
options: &LayoutOptions,
) -> LayoutBox {
let body_box = layout_node(base, options);
let base_w = body_box.width.max(0.5);
if label == "\\textcircled" {
return layout_textcircled(body_box, options);
}
if let Some((commands, w, h, fill)) =
crate::katex_svg::katex_accent_path(label, base_w, accent_ordgroup_len(base))
{
let accent_box = LayoutBox {
width: w,
height: 0.0,
depth: h,
content: BoxContent::SvgPath { commands, fill },
color: options.color,
};
let gap = 0.065;
let under_gap_em = if is_below && label == "\\utilde" {
0.12
} else {
0.0
};
let clearance = if is_below {
body_box.height + body_box.depth + gap
} else if label == "\\vec" {
(body_box.height - options.metrics().x_height).max(0.0)
} else {
body_box.height + gap
};
let (height, depth) = if is_below {
(body_box.height, body_box.depth + h + gap + under_gap_em)
} else if label == "\\vec" {
(clearance + h, body_box.depth)
} else {
(body_box.height + gap + h, body_box.depth)
};
let vec_skew = if label == "\\vec" {
(if is_shifty {
glyph_skew(&body_box)
} else {
0.0
}) + VEC_SKEW_EXTRA_RIGHT_EM
} else {
0.0
};
return LayoutBox {
width: body_box.width,
height,
depth,
content: BoxContent::Accent {
base: Box::new(body_box),
accent: Box::new(accent_box),
clearance,
skew: vec_skew,
is_below,
under_gap_em,
},
color: options.color,
};
}
let use_arrow_path = is_stretchy && is_arrow_accent(label);
let accent_box = if use_arrow_path {
let (commands, arrow_h, fill_arrow) =
match crate::katex_svg::katex_stretchy_path(label, base_w) {
Some((c, h)) => (c, h, true),
None => {
let h = 0.3_f64;
let c = stretchy_accent_path(label, base_w, h);
let fill = label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow";
(c, h, fill)
}
};
LayoutBox {
width: base_w,
height: arrow_h / 2.0,
depth: arrow_h / 2.0,
content: BoxContent::SvgPath {
commands,
fill: fill_arrow,
},
color: options.color,
}
} else {
let accent_char = {
let ch = resolve_symbol_char(label, Mode::Text);
if ch == label.chars().next().unwrap_or('?') {
resolve_symbol_char(label, Mode::Math)
} else {
ch
}
};
let accent_code = accent_char as u32;
let accent_metrics = get_char_metrics(FontId::MainRegular, accent_code);
let (accent_w, accent_h, accent_d) = match accent_metrics {
Some(m) => (m.width, m.height, m.depth),
None => (body_box.width, 0.25, 0.0),
};
LayoutBox {
width: accent_w,
height: accent_h,
depth: accent_d,
content: BoxContent::Glyph {
font_id: FontId::MainRegular,
char_code: accent_code,
},
color: options.color,
}
};
let skew = if use_arrow_path {
0.0
} else if is_shifty {
glyph_skew(&body_box)
} else {
0.0
};
let gap = if use_arrow_path {
if label == "\\Overrightarrow" {
0.21
} else {
0.26
}
} else {
0.0
};
let clearance = if is_below {
body_box.height + body_box.depth + accent_box.depth + gap
} else if use_arrow_path {
body_box.height + gap
} else {
let base_clearance = match &body_box.content {
BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. }
if !is_below =>
{
if inner_accent.height <= 0.001 {
let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
katex_pos + correction
} else {
inner_cl + 0.3
}
}
_ => {
if label == "\\bar" || label == "\\=" {
body_box.height
} else {
let katex_pos = (body_box.height - options.metrics().x_height).max(0.0);
let correction = (accent_box.height - 0.35_f64.min(accent_box.height)).max(0.0);
katex_pos + correction
}
}
};
let base_clearance = base_clearance + accent_box.depth;
if label == "\\bar" || label == "\\=" {
(base_clearance - 0.12).max(0.0)
} else {
base_clearance
}
};
let (height, depth) = if is_below {
(body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap)
} else if use_arrow_path {
(body_box.height + gap + accent_box.height, body_box.depth)
} else {
const ACCENT_ABOVE_STRUT_HEIGHT_EM: f64 = 0.78056;
let accent_visual_top = clearance + 0.35_f64.min(accent_box.height);
let h = if matches!(label, "\\hat" | "\\bar" | "\\=" | "\\dot" | "\\ddot") {
accent_visual_top.max(ACCENT_ABOVE_STRUT_HEIGHT_EM)
} else {
body_box.height.max(accent_visual_top)
};
(h, body_box.depth)
};
LayoutBox {
width: body_box.width,
height,
depth,
content: BoxContent::Accent {
base: Box::new(body_box),
accent: Box::new(accent_box),
clearance,
skew,
is_below,
under_gap_em: 0.0,
},
color: options.color,
}
}
fn node_contains_middle(node: &ParseNode) -> bool {
match node {
ParseNode::Middle { .. } => true,
ParseNode::OrdGroup { body, .. } | ParseNode::MClass { body, .. } => {
body.iter().any(node_contains_middle)
}
ParseNode::SupSub { base, sup, sub, .. } => {
base.as_deref().is_some_and(node_contains_middle)
|| sup.as_deref().is_some_and(node_contains_middle)
|| sub.as_deref().is_some_and(node_contains_middle)
}
ParseNode::GenFrac { numer, denom, .. } => {
node_contains_middle(numer) || node_contains_middle(denom)
}
ParseNode::Sqrt { body, index, .. } => {
node_contains_middle(body) || index.as_deref().is_some_and(node_contains_middle)
}
ParseNode::Accent { base, .. } | ParseNode::AccentUnder { base, .. } => {
node_contains_middle(base)
}
ParseNode::Op { body, .. } => body
.as_ref()
.is_some_and(|b| b.iter().any(node_contains_middle)),
ParseNode::LeftRight { body, .. } => body.iter().any(node_contains_middle),
ParseNode::OperatorName { body, .. } => body.iter().any(node_contains_middle),
ParseNode::Font { body, .. } => node_contains_middle(body),
ParseNode::Text { body, .. }
| ParseNode::Color { body, .. }
| ParseNode::Styling { body, .. }
| ParseNode::Sizing { body, .. } => body.iter().any(node_contains_middle),
ParseNode::Overline { body, .. } | ParseNode::Underline { body, .. } => {
node_contains_middle(body)
}
ParseNode::Phantom { body, .. } => body.iter().any(node_contains_middle),
ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => {
node_contains_middle(body)
}
ParseNode::Array { body, .. } => body
.iter()
.any(|row| row.iter().any(node_contains_middle)),
ParseNode::Enclose { body, .. }
| ParseNode::Lap { body, .. }
| ParseNode::RaiseBox { body, .. }
| ParseNode::VCenter { body, .. } => node_contains_middle(body),
ParseNode::Pmb { body, .. } => body.iter().any(node_contains_middle),
ParseNode::XArrow { body, below, .. } => {
node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle)
}
ParseNode::CdArrow { label_above, label_below, .. } => {
label_above.as_deref().is_some_and(node_contains_middle)
|| label_below.as_deref().is_some_and(node_contains_middle)
}
ParseNode::MathChoice {
display,
text,
script,
scriptscript,
..
} => {
display.iter().any(node_contains_middle)
|| text.iter().any(node_contains_middle)
|| script.iter().any(node_contains_middle)
|| scriptscript.iter().any(node_contains_middle)
}
ParseNode::HorizBrace { base, .. } => node_contains_middle(base),
ParseNode::Href { body, .. } => body.iter().any(node_contains_middle),
_ => false,
}
}
fn body_contains_middle(nodes: &[ParseNode]) -> bool {
nodes.iter().any(node_contains_middle)
}
fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 {
let m = options.metrics();
if options.style.is_display() {
m.delim1
} else if matches!(
options.style,
MathStyle::ScriptScript | MathStyle::ScriptScriptCramped
) {
options
.with_style(MathStyle::Script)
.metrics()
.delim2
} else {
m.delim2
}
}
fn left_right_delim_total_height(inner: &LayoutBox, options: &LayoutOptions) -> f64 {
let metrics = options.metrics();
let inner_height = inner.height;
let inner_depth = inner.depth;
let axis = metrics.axis_height;
let max_dist = (inner_height - axis).max(inner_depth + axis);
let delim_factor = 901.0;
let delim_extend = 5.0 / metrics.pt_per_em;
let from_formula = (max_dist / 500.0 * delim_factor).max(2.0 * max_dist - delim_extend);
from_formula.max(inner_height + inner_depth)
}
fn layout_left_right(
body: &[ParseNode],
left_delim: &str,
right_delim: &str,
options: &LayoutOptions,
) -> LayoutBox {
let (inner, total_height) = if body_contains_middle(body) {
let opts_first = LayoutOptions {
leftright_delim_height: None,
..options.clone()
};
let inner_first = layout_expression(body, &opts_first, true);
let total_height = left_right_delim_total_height(&inner_first, options);
let opts_second = LayoutOptions {
leftright_delim_height: Some(total_height),
..options.clone()
};
let inner_second = layout_expression(body, &opts_second, true);
(inner_second, total_height)
} else {
let inner = layout_expression(body, options, true);
let total_height = left_right_delim_total_height(&inner, options);
(inner, total_height)
};
let inner_height = inner.height;
let inner_depth = inner.depth;
let left_box = make_stretchy_delim(left_delim, total_height, options);
let right_box = make_stretchy_delim(right_delim, total_height, options);
let width = left_box.width + inner.width + right_box.width;
let height = left_box.height.max(right_box.height).max(inner_height);
let depth = left_box.depth.max(right_box.depth).max(inner_depth);
LayoutBox {
width,
height,
depth,
content: BoxContent::LeftRight {
left: Box::new(left_box),
right: Box::new(right_box),
inner: Box::new(inner),
},
color: options.color,
}
}
const DELIM_FONT_SEQUENCE: [FontId; 5] = [
FontId::MainRegular,
FontId::Size1Regular,
FontId::Size2Regular,
FontId::Size3Regular,
FontId::Size4Regular,
];
fn normalize_delim(delim: &str) -> &str {
match delim {
"<" | "\\lt" | "\u{27E8}" => "\\langle",
">" | "\\gt" | "\u{27E9}" => "\\rangle",
_ => delim,
}
}
fn is_vert_delim(delim: &str) -> bool {
matches!(delim, "|" | "\\vert" | "\\lvert" | "\\rvert")
}
fn is_double_vert_delim(delim: &str) -> bool {
matches!(delim, "\\|" | "\\Vert" | "\\lVert" | "\\rVert")
}
fn vert_repeat_piece_height(is_double: bool) -> f64 {
let code = if is_double { 8741_u32 } else { 8739 };
get_char_metrics(FontId::Size1Regular, code)
.map(|m| m.height + m.depth)
.unwrap_or(0.5)
}
fn katex_vert_real_height(requested_total: f64, is_double: bool) -> f64 {
let piece = vert_repeat_piece_height(is_double);
let min_h = 2.0 * piece;
let repeat_count = ((requested_total - min_h) / piece).ceil().max(0.0);
let mut h = min_h + repeat_count * piece;
if (requested_total - 3.0).abs() < 0.01 && !is_double {
h *= 1.135;
}
h
}
fn tall_vert_svg_path_data(mid_th: i64, is_double: bool) -> String {
let neg = -mid_th;
if !is_double {
format!(
"M145 15 v585 v{mid_th} v585 c2.667,10,9.667,15,21,15 c10,0,16.667,-5,20,-15 v-585 v{neg} v-585 c-2.667,-10,-9.667,-15,-21,-15 c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v{mid_th} v585 h43z"
)
} else {
format!(
"M145 15 v585 v{mid_th} v585 c2.667,10,9.667,15,21,15 c10,0,16.667,-5,20,-15 v-585 v{neg} v-585 c-2.667,-10,-9.667,-15,-21,-15 c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v{mid_th} v585 h43z M367 15 v585 v{mid_th} v585 c2.667,10,9.667,15,21,15 c10,0,16.667,-5,20,-15 v-585 v{neg} v-585 c-2.667,-10,-9.667,-15,-21,-15 c-10,0,-16.667,5,-20,15z M410 15 H367 v585 v{mid_th} v585 h43z"
)
}
}
fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec<PathCommand> {
let s = 0.001_f64;
cmds.iter()
.map(|c| match *c {
PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
x: x * s,
y: y * s,
},
PathCommand::LineTo { x, y } => PathCommand::LineTo {
x: x * s,
y: y * s,
},
PathCommand::CubicTo {
x1,
y1,
x2,
y2,
x,
y,
} => PathCommand::CubicTo {
x1: x1 * s,
y1: y1 * s,
x2: x2 * s,
y2: y2 * s,
x: x * s,
y: y * s,
},
PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
x1: x1 * s,
y1: y1 * s,
x: x * s,
y: y * s,
},
PathCommand::Close => PathCommand::Close,
})
.collect()
}
fn map_vert_path_y_to_baseline(
cmds: Vec<PathCommand>,
height: f64,
depth: f64,
view_box_height: i64,
) -> Vec<PathCommand> {
let span_em = view_box_height as f64 / 1000.0;
let total = height + depth;
let scale_y = if span_em > 0.0 { total / span_em } else { 1.0 };
cmds.into_iter()
.map(|c| match c {
PathCommand::MoveTo { x, y } => PathCommand::MoveTo {
x,
y: -height + y * scale_y,
},
PathCommand::LineTo { x, y } => PathCommand::LineTo {
x,
y: -height + y * scale_y,
},
PathCommand::CubicTo {
x1,
y1,
x2,
y2,
x,
y,
} => PathCommand::CubicTo {
x1,
y1: -height + y1 * scale_y,
x2,
y2: -height + y2 * scale_y,
x,
y: -height + y * scale_y,
},
PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
x1,
y1: -height + y1 * scale_y,
x,
y: -height + y * scale_y,
},
PathCommand::Close => PathCommand::Close,
})
.collect()
}
fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptions) -> LayoutBox {
let real_h = katex_vert_real_height(total_height, is_double);
let axis = options.metrics().axis_height;
let depth = (real_h / 2.0 - axis).max(0.0);
let height = real_h - depth;
let width = if is_double { 0.556 } else { 0.333 };
let piece = vert_repeat_piece_height(is_double);
let mid_em = (real_h - 2.0 * piece).max(0.0);
let mid_th = (mid_em * 1000.0).round() as i64;
let view_box_height = (real_h * 1000.0).round() as i64;
let d = tall_vert_svg_path_data(mid_th, is_double);
let raw = parse_svg_path_data(&d);
let scaled = scale_svg_path_to_em(&raw);
let commands = map_vert_path_y_to_baseline(scaled, height, depth, view_box_height);
LayoutBox {
width,
height,
depth,
content: BoxContent::SvgPath { commands, fill: true },
color: options.color,
}
}
fn make_stretchy_delim(delim: &str, total_height: f64, options: &LayoutOptions) -> LayoutBox {
if delim == "." || delim.is_empty() {
return LayoutBox::new_kern(0.0);
}
const VERT_NATURAL_HEIGHT: f64 = 1.0; if is_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
return make_vert_delim_box(total_height, false, options);
}
if is_double_vert_delim(delim) && total_height > VERT_NATURAL_HEIGHT {
return make_vert_delim_box(total_height, true, options);
}
let delim = normalize_delim(delim);
let ch = resolve_symbol_char(delim, Mode::Math);
let char_code = ch as u32;
let mut best_font = FontId::MainRegular;
let mut best_w = 0.4;
let mut best_h = 0.7;
let mut best_d = 0.2;
for &font_id in &DELIM_FONT_SEQUENCE {
if let Some(m) = get_char_metrics(font_id, char_code) {
best_font = font_id;
best_w = m.width;
best_h = m.height;
best_d = m.depth;
if best_h + best_d >= total_height {
break;
}
}
}
let best_total = best_h + best_d;
if let Some(stacked) = make_stacked_delim_if_needed(delim, total_height, best_total, options) {
return stacked;
}
LayoutBox {
width: best_w,
height: best_h,
depth: best_d,
content: BoxContent::Glyph {
font_id: best_font,
char_code,
},
color: options.color,
}
}
const SIZE_TO_MAX_HEIGHT: [f64; 5] = [0.0, 1.2, 1.8, 2.4, 3.0];
fn layout_delim_sizing(size: u8, delim: &str, options: &LayoutOptions) -> LayoutBox {
if delim == "." || delim.is_empty() {
return LayoutBox::new_kern(0.0);
}
if is_vert_delim(delim) {
let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
return make_vert_delim_box(total, false, options);
}
if is_double_vert_delim(delim) {
let total = SIZE_TO_MAX_HEIGHT[size.min(4) as usize];
return make_vert_delim_box(total, true, options);
}
let delim = normalize_delim(delim);
let ch = resolve_symbol_char(delim, Mode::Math);
let char_code = ch as u32;
let font_id = match size {
1 => FontId::Size1Regular,
2 => FontId::Size2Regular,
3 => FontId::Size3Regular,
4 => FontId::Size4Regular,
_ => FontId::Size1Regular,
};
let metrics = get_char_metrics(font_id, char_code);
let (width, height, depth, actual_font) = match metrics {
Some(m) => (m.width, m.height, m.depth, font_id),
None => {
let m = get_char_metrics(FontId::MainRegular, char_code);
match m {
Some(m) => (m.width, m.height, m.depth, FontId::MainRegular),
None => (0.4, 0.7, 0.2, FontId::MainRegular),
}
}
};
LayoutBox {
width,
height,
depth,
content: BoxContent::Glyph {
font_id: actual_font,
char_code,
},
color: options.color,
}
}
#[allow(clippy::too_many_arguments)]
fn layout_array(
body: &[Vec<ParseNode>],
cols: Option<&[ratex_parser::parse_node::AlignSpec]>,
arraystretch: f64,
add_jot: bool,
row_gaps: &[Option<ratex_parser::parse_node::Measurement>],
hlines: &[Vec<bool>],
col_sep_type: Option<&str>,
hskip: bool,
options: &LayoutOptions,
) -> LayoutBox {
let metrics = options.metrics();
let pt = 1.0 / metrics.pt_per_em;
let baselineskip = 12.0 * pt;
let jot = 3.0 * pt;
let arrayskip = arraystretch * baselineskip;
let arstrut_h = 0.7 * arrayskip;
let arstrut_d = 0.3 * arrayskip;
const ALIGN_RELATION_MU: f64 = 3.0;
let col_gap = match col_sep_type {
Some("align") => mu_to_em(ALIGN_RELATION_MU, metrics.quad),
Some("alignat") => 0.0,
Some("small") => {
2.0 * mu_to_em(5.0, metrics.quad) * MathStyle::Script.size_multiplier()
/ options.size_multiplier()
}
_ => 2.0 * 5.0 * pt, };
let cell_options = match col_sep_type {
Some("align") | Some("alignat") => LayoutOptions {
align_relation_spacing: Some(ALIGN_RELATION_MU),
..options.clone()
},
_ => options.clone(),
};
let num_rows = body.len();
if num_rows == 0 {
return LayoutBox::new_empty();
}
let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
use ratex_parser::parse_node::AlignType;
let col_aligns: Vec<u8> = {
let align_specs: Vec<&ratex_parser::parse_node::AlignSpec> = cols
.map(|cs| {
cs.iter()
.filter(|s| matches!(s.align_type, AlignType::Align))
.collect()
})
.unwrap_or_default();
(0..num_cols)
.map(|c| {
align_specs
.get(c)
.and_then(|s| s.align.as_deref())
.and_then(|a| a.bytes().next())
.unwrap_or(b'c')
})
.collect()
};
let col_separators: Vec<Option<bool>> = {
let mut seps = vec![None; num_cols + 1];
let mut align_count = 0usize;
if let Some(cs) = cols {
for spec in cs {
match spec.align_type {
AlignType::Align => align_count += 1,
AlignType::Separator if spec.align.as_deref() == Some("|") => {
if align_count <= num_cols {
seps[align_count] = Some(false);
}
}
AlignType::Separator if spec.align.as_deref() == Some(":") => {
if align_count <= num_cols {
seps[align_count] = Some(true);
}
}
_ => {}
}
}
}
seps
};
let rule_thickness = 0.4 * pt;
let double_rule_sep = metrics.double_rule_sep;
let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
let mut col_widths = vec![0.0_f64; num_cols];
let mut row_heights = Vec::with_capacity(num_rows);
let mut row_depths = Vec::with_capacity(num_rows);
for row in body {
let mut row_boxes = Vec::with_capacity(num_cols);
let mut rh = arstrut_h;
let mut rd = arstrut_d;
for (c, cell) in row.iter().enumerate() {
let cell_nodes = match cell {
ParseNode::OrdGroup { body, .. } => body.as_slice(),
other => std::slice::from_ref(other),
};
let cell_box = layout_expression(cell_nodes, &cell_options, true);
rh = rh.max(cell_box.height);
rd = rd.max(cell_box.depth);
if c < num_cols {
col_widths[c] = col_widths[c].max(cell_box.width);
}
row_boxes.push(cell_box);
}
while row_boxes.len() < num_cols {
row_boxes.push(LayoutBox::new_empty());
}
if add_jot {
rd += jot;
}
row_heights.push(rh);
row_depths.push(rd);
cell_boxes.push(row_boxes);
}
for (r, gap) in row_gaps.iter().enumerate() {
if r < row_depths.len() {
if let Some(m) = gap {
let gap_em = measurement_to_em(m, options);
if gap_em > 0.0 {
row_depths[r] = row_depths[r].max(gap_em + arstrut_d);
}
}
}
}
let mut hlines_before_row: Vec<Vec<bool>> = hlines.to_vec();
while hlines_before_row.len() < num_rows + 1 {
hlines_before_row.push(vec![]);
}
for r in 0..=num_rows {
let n = hlines_before_row[r].len();
if n > 1 {
let extra = (n - 1) as f64 * (rule_thickness + double_rule_sep);
if r == 0 {
if num_rows > 0 {
row_heights[0] += extra;
}
} else {
row_depths[r - 1] += extra;
}
}
}
let mut total_height = 0.0;
let mut row_positions = Vec::with_capacity(num_rows);
for r in 0..num_rows {
total_height += row_heights[r];
row_positions.push(total_height);
total_height += row_depths[r];
}
let offset = total_height / 2.0 + metrics.axis_height;
let content_x_offset = if hskip { col_gap / 2.0 } else { 0.0 };
let total_width: f64 = col_widths.iter().sum::<f64>()
+ col_gap * (num_cols.saturating_sub(1)) as f64
+ 2.0 * content_x_offset;
let height = offset;
let depth = total_height - offset;
LayoutBox {
width: total_width,
height,
depth,
content: BoxContent::Array {
cells: cell_boxes,
col_widths: col_widths.clone(),
col_aligns,
row_heights: row_heights.clone(),
row_depths: row_depths.clone(),
col_gap,
offset,
content_x_offset,
col_separators,
hlines_before_row,
rule_thickness,
double_rule_sep,
},
color: options.color,
}
}
fn layout_sizing(size: u8, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
let multiplier = match size {
1 => 0.5,
2 => 0.6,
3 => 0.7,
4 => 0.8,
5 => 0.9,
6 => 1.0,
7 => 1.2,
8 => 1.44,
9 => 1.728,
10 => 2.074,
11 => 2.488,
_ => 1.0,
};
let inner = layout_expression(body, options, true);
let ratio = multiplier / options.size_multiplier();
if (ratio - 1.0).abs() < 0.001 {
inner
} else {
LayoutBox {
width: inner.width * ratio,
height: inner.height * ratio,
depth: inner.depth * ratio,
content: BoxContent::Scaled {
body: Box::new(inner),
child_scale: ratio,
},
color: options.color,
}
}
}
fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox {
let metrics = options.metrics();
let mut children = Vec::new();
for c in body.chars() {
let ch = if star && c == ' ' {
'\u{2423}' } else {
c
};
let code = ch as u32;
let (font_id, w, h, d) = match get_char_metrics(FontId::TypewriterRegular, code) {
Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth),
None => match get_char_metrics(FontId::MainRegular, code) {
Some(m) => (FontId::MainRegular, m.width, m.height, m.depth),
None => (
FontId::TypewriterRegular,
0.5,
metrics.x_height,
0.0,
),
},
};
children.push(LayoutBox {
width: w,
height: h,
depth: d,
content: BoxContent::Glyph {
font_id,
char_code: code,
},
color: options.color,
});
}
let mut hbox = make_hbox(children);
hbox.color = options.color;
hbox
}
fn layout_text(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
let mut children = Vec::new();
for node in body {
match node {
ParseNode::TextOrd { text, mode, .. } | ParseNode::MathOrd { text, mode, .. } => {
children.push(layout_symbol(text, *mode, options));
}
ParseNode::SpacingNode { text, .. } => {
children.push(layout_spacing_command(text, options));
}
_ => {
children.push(layout_node(node, options));
}
}
}
make_hbox(children)
}
fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
let base = layout_expression(body, options, true);
let w = base.width;
let h = base.height;
let d = base.depth;
let shadow = layout_expression(body, options, true);
let shadow_shift_x = 0.02_f64;
let _shadow_shift_y = 0.01_f64;
let kern_back = LayoutBox::new_kern(-w);
let kern_x = LayoutBox::new_kern(shadow_shift_x);
let children = vec![
kern_x,
shadow,
kern_back,
base,
];
let hbox = make_hbox(children);
LayoutBox {
width: w,
height: h,
depth: d,
content: hbox.content,
color: options.color,
}
}
fn layout_enclose(
label: &str,
background_color: Option<&str>,
border_color: Option<&str>,
body: &ParseNode,
options: &LayoutOptions,
) -> LayoutBox {
use crate::layout_box::BoxContent;
use ratex_types::color::Color;
if label == "\\phase" {
return layout_phase(body, options);
}
if label == "\\angl" {
return layout_angl(body, options);
}
if matches!(label, "\\cancel" | "\\bcancel" | "\\xcancel" | "\\sout") {
return layout_cancel(label, body, options);
}
let metrics = options.metrics();
let padding = 3.0 / metrics.pt_per_em;
let border_thickness = 0.4 / metrics.pt_per_em;
let has_border = matches!(label, "\\fbox" | "\\fcolorbox");
let bg = background_color.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)));
let border = border_color
.and_then(|c| Color::from_name(c).or_else(|| Color::from_hex(c)))
.unwrap_or(Color::BLACK);
let inner = layout_node(body, options);
let outer_pad = padding + if has_border { border_thickness } else { 0.0 };
let width = inner.width + 2.0 * outer_pad;
let height = inner.height + outer_pad;
let depth = inner.depth + outer_pad;
LayoutBox {
width,
height,
depth,
content: BoxContent::Framed {
body: Box::new(inner),
padding,
border_thickness,
has_border,
bg_color: bg,
border_color: border,
},
color: options.color,
}
}
fn layout_raisebox(shift: f64, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
use crate::layout_box::BoxContent;
let inner = layout_node(body, options);
let height = inner.height + shift;
let depth = (inner.depth - shift).max(0.0);
let width = inner.width;
LayoutBox {
width,
height,
depth,
content: BoxContent::RaiseBox {
body: Box::new(inner),
shift,
},
color: options.color,
}
}
fn is_single_char_body(node: &ParseNode) -> bool {
use ratex_parser::parse_node::ParseNode as PN;
match node {
PN::OrdGroup { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
PN::Styling { body, .. } if body.len() == 1 => is_single_char_body(&body[0]),
PN::Atom { .. } | PN::MathOrd { .. } | PN::TextOrd { .. } => true,
_ => false,
}
}
fn layout_cancel(
label: &str,
body: &ParseNode,
options: &LayoutOptions,
) -> LayoutBox {
use crate::layout_box::BoxContent;
let inner = layout_node(body, options);
let w = inner.width.max(0.01);
let h = inner.height;
let d = inner.depth;
let single = is_single_char_body(body);
let (v_pad, h_pad) = if label == "\\sout" {
(0.0, 0.0)
} else if single {
(0.2, 0.0)
} else {
(0.0, 0.2)
};
let commands: Vec<PathCommand> = match label {
"\\cancel" => vec![
PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, ],
"\\bcancel" => vec![
PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, ],
"\\xcancel" => vec![
PathCommand::MoveTo { x: -h_pad, y: d + v_pad },
PathCommand::LineTo { x: w + h_pad, y: -h - v_pad },
PathCommand::MoveTo { x: -h_pad, y: -h - v_pad },
PathCommand::LineTo { x: w + h_pad, y: d + v_pad },
],
"\\sout" => {
let mid_y = -0.5 * options.metrics().x_height;
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: w, y: mid_y },
]
}
_ => vec![],
};
let line_w = w + 2.0 * h_pad;
let line_h = h + v_pad;
let line_d = d + v_pad;
let line_box = LayoutBox {
width: line_w,
height: line_h,
depth: line_d,
content: BoxContent::SvgPath { commands, fill: false },
color: options.color,
};
let body_kern = -(line_w - h_pad);
let body_shifted = make_hbox(vec![LayoutBox::new_kern(body_kern), inner]);
LayoutBox {
width: w,
height: h,
depth: d,
content: BoxContent::HBox(vec![line_box, body_shifted]),
color: options.color,
}
}
fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
use crate::layout_box::BoxContent;
let metrics = options.metrics();
let inner = layout_node(body, options);
let line_weight = 0.6_f64 / metrics.pt_per_em;
let clearance = 0.35_f64 * metrics.x_height;
let angle_height = inner.height + inner.depth + line_weight + clearance;
let left_pad = angle_height / 2.0 + line_weight;
let width = inner.width + left_pad;
let y_svg = (1000.0 * angle_height).floor().max(80.0);
let sy = angle_height / y_svg;
let sx = sy;
let right_x = (400_000.0_f64 * sx).min(width);
let bottom_y = inner.depth + line_weight + clearance;
let vy = |y_sv: f64| -> f64 { bottom_y - (y_svg - y_sv) * sy };
let x_peak = y_svg / 2.0;
let commands = vec![
PathCommand::MoveTo { x: right_x, y: vy(y_svg) },
PathCommand::LineTo { x: 0.0, y: vy(y_svg) },
PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) },
PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) },
PathCommand::LineTo {
x: 145.0 * sx,
y: vy(y_svg - 80.0),
},
PathCommand::LineTo {
x: right_x,
y: vy(y_svg - 80.0),
},
PathCommand::Close,
];
let body_shifted = make_hbox(vec![
LayoutBox::new_kern(left_pad),
inner.clone(),
]);
let path_height = inner.height;
let path_depth = bottom_y;
LayoutBox {
width,
height: path_height,
depth: path_depth,
content: BoxContent::HBox(vec![
LayoutBox {
width,
height: path_height,
depth: path_depth,
content: BoxContent::SvgPath { commands, fill: true },
color: options.color,
},
LayoutBox::new_kern(-width),
body_shifted,
]),
color: options.color,
}
}
fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
use crate::layout_box::BoxContent;
let inner = layout_node(body, options);
let w = inner.width.max(0.3);
let clearance = 0.1_f64;
let arc_h = inner.height + clearance;
let path_commands = vec![
PathCommand::MoveTo { x: 0.0, y: -arc_h },
PathCommand::LineTo { x: w, y: -arc_h },
PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64},
];
let height = arc_h;
LayoutBox {
width: w,
height,
depth: inner.depth,
content: BoxContent::Angl {
path_commands,
body: Box::new(inner),
},
color: options.color,
}
}
fn layout_font(font: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
let font_id = match font {
"mathrm" | "\\mathrm" | "textrm" | "\\textrm" | "rm" | "\\rm" => Some(FontId::MainRegular),
"mathbf" | "\\mathbf" | "textbf" | "\\textbf" | "bf" | "\\bf" => Some(FontId::MainBold),
"mathit" | "\\mathit" | "textit" | "\\textit" => Some(FontId::MainItalic),
"mathsf" | "\\mathsf" | "textsf" | "\\textsf" => Some(FontId::SansSerifRegular),
"mathtt" | "\\mathtt" | "texttt" | "\\texttt" => Some(FontId::TypewriterRegular),
"mathcal" | "\\mathcal" | "cal" | "\\cal" => Some(FontId::CaligraphicRegular),
"mathfrak" | "\\mathfrak" | "frak" | "\\frak" => Some(FontId::FrakturRegular),
"mathscr" | "\\mathscr" => Some(FontId::ScriptRegular),
"mathbb" | "\\mathbb" => Some(FontId::AmsRegular),
"boldsymbol" | "\\boldsymbol" | "bm" | "\\bm" => Some(FontId::MathBoldItalic),
_ => None,
};
if let Some(fid) = font_id {
layout_with_font(body, fid, options)
} else {
layout_node(body, options)
}
}
fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) -> LayoutBox {
match node {
ParseNode::OrdGroup { body, .. } => {
let kern = options.inter_glyph_kern_em;
let mut children: Vec<LayoutBox> = Vec::with_capacity(body.len().saturating_mul(2));
for (i, n) in body.iter().enumerate() {
if i > 0 && kern > 0.0 {
children.push(LayoutBox::new_kern(kern));
}
children.push(layout_with_font(n, font_id, options));
}
make_hbox(children)
}
ParseNode::SupSub {
base, sup, sub, ..
} => {
if let Some(base_node) = base.as_deref() {
if should_use_op_limits(base_node, options) {
return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options);
}
}
layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id))
}
ParseNode::MathOrd { text, .. }
| ParseNode::TextOrd { text, .. }
| ParseNode::Atom { text, .. } => {
let ch = resolve_symbol_char(text, Mode::Math);
let char_code = ch as u32;
let metric_cp = ratex_font::font_and_metric_for_mathematical_alphanumeric(char_code)
.map(|(_, m)| m)
.unwrap_or(char_code);
if let Some(m) = get_char_metrics(font_id, metric_cp) {
LayoutBox {
width: math_glyph_advance_em(&m, Mode::Math),
height: m.height,
depth: m.depth,
content: BoxContent::Glyph { font_id, char_code },
color: options.color,
}
} else {
layout_node(node, options)
}
}
_ => layout_node(node, options),
}
}
fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
let cramped = options.with_style(options.style.cramped());
let body_box = layout_node(body, &cramped);
let metrics = options.metrics();
let rule = metrics.default_rule_thickness;
let height = body_box.height + 3.0 * rule;
LayoutBox {
width: body_box.width,
height,
depth: body_box.depth,
content: BoxContent::Overline {
body: Box::new(body_box),
rule_thickness: rule,
},
color: options.color,
}
}
fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
let body_box = layout_node(body, options);
let metrics = options.metrics();
let rule = metrics.default_rule_thickness;
let depth = body_box.depth + 3.0 * rule;
LayoutBox {
width: body_box.width,
height: body_box.height,
depth,
content: BoxContent::Underline {
body: Box::new(body_box),
rule_thickness: rule,
},
color: options.color,
}
}
fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox {
let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0));
let body_opts = options
.with_color(link_color)
.with_inter_glyph_kern(0.024);
let body_box = layout_expression(body, &body_opts, true);
layout_underline_laid_out(body_box, options, link_color)
}
fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox {
let metrics = options.metrics();
let rule = metrics.default_rule_thickness;
let depth = body_box.depth + 3.0 * rule;
LayoutBox {
width: body_box.width,
height: body_box.height,
depth,
content: BoxContent::Underline {
body: Box::new(body_box),
rule_thickness: rule,
},
color,
}
}
fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
let metrics = options.metrics();
let mu = metrics.css_em_per_mu();
let width = match text {
"\\," | "\\thinspace" => 3.0 * mu,
"\\:" | "\\medspace" => 4.0 * mu,
"\\;" | "\\thickspace" => 5.0 * mu,
"\\!" | "\\negthinspace" => -3.0 * mu,
"\\negmedspace" => -4.0 * mu,
"\\negthickspace" => -5.0 * mu,
" " | "~" | "\\nobreakspace" | "\\ " | "\\space" => {
get_char_metrics(FontId::MainRegular, 160)
.map(|m| m.width)
.unwrap_or(0.25)
}
"\\quad" => metrics.quad,
"\\qquad" => 2.0 * metrics.quad,
"\\enspace" => metrics.quad / 2.0,
_ => 0.0,
};
LayoutBox::new_kern(width)
}
fn measurement_to_em(m: &ratex_parser::parse_node::Measurement, options: &LayoutOptions) -> f64 {
let metrics = options.metrics();
match m.unit.as_str() {
"em" => m.number,
"ex" => m.number * metrics.x_height,
"mu" => m.number * metrics.css_em_per_mu(),
"pt" => m.number / metrics.pt_per_em,
"mm" => m.number * 7227.0 / 2540.0 / metrics.pt_per_em,
"cm" => m.number * 7227.0 / 254.0 / metrics.pt_per_em,
"in" => m.number * 72.27 / metrics.pt_per_em,
"bp" => m.number * 803.0 / 800.0 / metrics.pt_per_em,
"pc" => m.number * 12.0 / metrics.pt_per_em,
"dd" => m.number * 1238.0 / 1157.0 / metrics.pt_per_em,
"cc" => m.number * 14856.0 / 1157.0 / metrics.pt_per_em,
"nd" => m.number * 685.0 / 642.0 / metrics.pt_per_em,
"nc" => m.number * 1370.0 / 107.0 / metrics.pt_per_em,
"sp" => m.number / 65536.0 / metrics.pt_per_em,
_ => m.number,
}
}
fn node_math_class(node: &ParseNode) -> Option<MathClass> {
match node {
ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord),
ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)),
ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op),
ParseNode::OrdGroup { .. } => Some(MathClass::Ord),
ParseNode::GenFrac { left_delim, right_delim, .. } => {
let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".")
|| right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".");
if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) }
}
ParseNode::Sqrt { .. } => Some(MathClass::Ord),
ParseNode::SupSub { base, .. } => {
base.as_ref().and_then(|b| node_math_class(b))
}
ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
ParseNode::SpacingNode { .. } => None,
ParseNode::Kern { .. } => None,
ParseNode::HtmlMathMl { html, .. } => {
for child in html {
if let Some(cls) = node_math_class(child) {
return Some(cls);
}
}
None
}
ParseNode::Lap { .. } => None,
ParseNode::LeftRight { .. } => Some(MathClass::Inner),
ParseNode::AccentToken { .. } => Some(MathClass::Ord),
ParseNode::XArrow { .. } => Some(MathClass::Rel),
ParseNode::CdArrow { .. } => Some(MathClass::Rel),
ParseNode::DelimSizing { mclass, .. } => Some(mclass_str_to_math_class(mclass)),
ParseNode::Middle { .. } => Some(MathClass::Ord),
_ => Some(MathClass::Ord),
}
}
fn mclass_str_to_math_class(mclass: &str) -> MathClass {
match mclass {
"mord" => MathClass::Ord,
"mop" => MathClass::Op,
"mbin" => MathClass::Bin,
"mrel" => MathClass::Rel,
"mopen" => MathClass::Open,
"mclose" => MathClass::Close,
"mpunct" => MathClass::Punct,
"minner" => MathClass::Inner,
_ => MathClass::Ord,
}
}
fn is_character_box(node: &ParseNode) -> bool {
matches!(
node,
ParseNode::MathOrd { .. }
| ParseNode::TextOrd { .. }
| ParseNode::Atom { .. }
| ParseNode::AccentToken { .. }
)
}
fn family_to_math_class(family: AtomFamily) -> MathClass {
match family {
AtomFamily::Bin => MathClass::Bin,
AtomFamily::Rel => MathClass::Rel,
AtomFamily::Open => MathClass::Open,
AtomFamily::Close => MathClass::Close,
AtomFamily::Punct => MathClass::Punct,
AtomFamily::Inner => MathClass::Inner,
}
}
fn layout_horiz_brace(
base: &ParseNode,
is_over: bool,
func_label: &str,
options: &LayoutOptions,
) -> LayoutBox {
let body_box = layout_node(base, options);
let w = body_box.width.max(0.5);
let is_bracket = func_label
.trim_start_matches('\\')
.ends_with("bracket");
let stretch_key = if is_bracket {
if is_over {
"overbracket"
} else {
"underbracket"
}
} else if is_over {
"overbrace"
} else {
"underbrace"
};
let (raw_commands, brace_h, brace_fill) =
match crate::katex_svg::katex_stretchy_path(stretch_key, w) {
Some((c, h)) => (c, h, true),
None => {
let h = 0.35_f64;
(horiz_brace_path(w, h, is_over), h, false)
}
};
let y_shift = if is_over {
-brace_h / 2.0
} else {
brace_h / 2.0
};
let commands = shift_path_y(raw_commands, y_shift);
let brace_box = LayoutBox {
width: w,
height: if is_over { brace_h } else { 0.0 },
depth: if is_over { 0.0 } else { brace_h },
content: BoxContent::SvgPath {
commands,
fill: brace_fill,
},
color: options.color,
};
let gap = 0.1;
let (height, depth) = if is_over {
(body_box.height + brace_h + gap, body_box.depth)
} else {
(body_box.height, body_box.depth + brace_h + gap)
};
let clearance = if is_over {
height - brace_h
} else {
body_box.height + body_box.depth + gap
};
let total_w = body_box.width;
LayoutBox {
width: total_w,
height,
depth,
content: BoxContent::Accent {
base: Box::new(body_box),
accent: Box::new(brace_box),
clearance,
skew: 0.0,
is_below: !is_over,
under_gap_em: 0.0,
},
color: options.color,
}
}
fn layout_xarrow(
label: &str,
body: &ParseNode,
below: Option<&ParseNode>,
options: &LayoutOptions,
) -> LayoutBox {
let sup_style = options.style.superscript();
let sub_style = options.style.subscript();
let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
let sup_opts = options.with_style(sup_style);
let body_box = layout_node(body, &sup_opts);
let body_w = body_box.width * sup_ratio;
let below_box = below.map(|b| {
let sub_opts = options.with_style(sub_style);
layout_node(b, &sub_opts)
});
let below_w = below_box
.as_ref()
.map(|b| b.width * sub_ratio)
.unwrap_or(0.0);
let min_w = crate::katex_svg::katex_stretchy_min_width_em(label).unwrap_or(1.0);
let upper_w = body_w + sup_ratio;
let lower_w = if below_box.is_some() {
below_w + sub_ratio
} else {
0.0
};
let arrow_w = upper_w.max(lower_w).max(min_w);
let arrow_h = 0.3;
let (commands, actual_arrow_h, fill_arrow) =
match crate::katex_svg::katex_stretchy_path(label, arrow_w) {
Some((c, h)) => (c, h, true),
None => (
stretchy_accent_path(label, arrow_w, arrow_h),
arrow_h,
label == "\\xtwoheadrightarrow" || label == "\\xtwoheadleftarrow",
),
};
let arrow_box = LayoutBox {
width: arrow_w,
height: actual_arrow_h / 2.0,
depth: actual_arrow_h / 2.0,
content: BoxContent::SvgPath {
commands,
fill: fill_arrow,
},
color: options.color,
};
let metrics = options.metrics();
let axis = metrics.axis_height; let arrow_half = actual_arrow_h / 2.0;
let gap = 0.111;
let base_shift = -axis;
let sup_kern = gap;
let sub_kern = gap;
let sup_h = body_box.height * sup_ratio;
let sup_d = body_box.depth * sup_ratio;
let height = axis + arrow_half + gap + sup_h + sup_d;
let mut depth = (arrow_half - axis).max(0.0);
if let Some(ref bel) = below_box {
let sub_h = bel.height * sub_ratio;
let sub_d = bel.depth * sub_ratio;
depth = (arrow_half - axis) + gap + sub_h + sub_d;
}
LayoutBox {
width: arrow_w,
height,
depth,
content: BoxContent::OpLimits {
base: Box::new(arrow_box),
sup: Some(Box::new(body_box)),
sub: below_box.map(Box::new),
base_shift,
sup_kern,
sub_kern,
slant: 0.0,
sup_scale: sup_ratio,
sub_scale: sub_ratio,
},
color: options.color,
}
}
fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox {
let pad = 0.1_f64; let total_h = body_box.height + body_box.depth;
let radius = (body_box.width.max(total_h) / 2.0 + pad).max(0.35);
let diameter = radius * 2.0;
let cx = radius;
let cy = -(body_box.height - total_h / 2.0); let k = 0.5523; let r = radius;
let circle_commands = vec![
PathCommand::MoveTo { x: cx + r, y: cy },
PathCommand::CubicTo {
x1: cx + r, y1: cy - k * r,
x2: cx + k * r, y2: cy - r,
x: cx, y: cy - r,
},
PathCommand::CubicTo {
x1: cx - k * r, y1: cy - r,
x2: cx - r, y2: cy - k * r,
x: cx - r, y: cy,
},
PathCommand::CubicTo {
x1: cx - r, y1: cy + k * r,
x2: cx - k * r, y2: cy + r,
x: cx, y: cy + r,
},
PathCommand::CubicTo {
x1: cx + k * r, y1: cy + r,
x2: cx + r, y2: cy + k * r,
x: cx + r, y: cy,
},
PathCommand::Close,
];
let circle_box = LayoutBox {
width: diameter,
height: r - cy.min(0.0),
depth: (r + cy).max(0.0),
content: BoxContent::SvgPath {
commands: circle_commands,
fill: false,
},
color: options.color,
};
let content_shift = (diameter - body_box.width) / 2.0;
let children = vec![
circle_box,
LayoutBox::new_kern(-(diameter) + content_shift),
body_box.clone(),
];
let height = r - cy.min(0.0);
let depth = (r + cy).max(0.0);
LayoutBox {
width: diameter,
height,
depth,
content: BoxContent::HBox(children),
color: options.color,
}
}
fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox {
let r: f64 = 0.1125;
let cy: f64 = -0.2625;
let k: f64 = 0.5523;
let cx: f64 = r;
let h: f64 = r + cy.abs(); let d: f64 = 0.0;
let stroke_half: f64 = 0.01875; let r_ring: f64 = r - stroke_half;
let circle_commands = |ox: f64, rad: f64| -> Vec<PathCommand> {
vec![
PathCommand::MoveTo { x: ox + rad, y: cy },
PathCommand::CubicTo {
x1: ox + rad, y1: cy - k * rad,
x2: ox + k * rad, y2: cy - rad,
x: ox, y: cy - rad,
},
PathCommand::CubicTo {
x1: ox - k * rad, y1: cy - rad,
x2: ox - rad, y2: cy - k * rad,
x: ox - rad, y: cy,
},
PathCommand::CubicTo {
x1: ox - rad, y1: cy + k * rad,
x2: ox - k * rad, y2: cy + rad,
x: ox, y: cy + rad,
},
PathCommand::CubicTo {
x1: ox + k * rad, y1: cy + rad,
x2: ox + rad, y2: cy + k * rad,
x: ox + rad, y: cy,
},
PathCommand::Close,
]
};
let disk = LayoutBox {
width: 2.0 * r,
height: h,
depth: d,
content: BoxContent::SvgPath {
commands: circle_commands(cx, r),
fill: true,
},
color: options.color,
};
let ring = LayoutBox {
width: 2.0 * r,
height: h,
depth: d,
content: BoxContent::SvgPath {
commands: circle_commands(cx, r_ring),
fill: false,
},
color: options.color,
};
let bar_len: f64 = 0.25;
let bar_th: f64 = 0.04;
let bar_raise: f64 = cy.abs() - bar_th / 2.0;
let bar = LayoutBox::new_rule(bar_len, h, d, bar_th, bar_raise);
let children = if imageof {
vec![disk, bar, ring]
} else {
vec![ring, bar, disk]
};
let total_width = 4.0 * r + bar_len;
LayoutBox {
width: total_width,
height: h,
depth: d,
content: BoxContent::HBox(children),
color: options.color,
}
}
fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec<PathCommand> {
let cx = width / 2.0;
let cy = (depth - height) / 2.0; let a = width * 0.402_f64; let b = 0.3_f64; let k = 0.62_f64; vec![
PathCommand::MoveTo { x: cx + a, y: cy },
PathCommand::CubicTo {
x1: cx + a,
y1: cy - k * b,
x2: cx + k * a,
y2: cy - b,
x: cx,
y: cy - b,
},
PathCommand::CubicTo {
x1: cx - k * a,
y1: cy - b,
x2: cx - a,
y2: cy - k * b,
x: cx - a,
y: cy,
},
PathCommand::CubicTo {
x1: cx - a,
y1: cy + k * b,
x2: cx - k * a,
y2: cy + b,
x: cx,
y: cy + b,
},
PathCommand::CubicTo {
x1: cx + k * a,
y1: cy + b,
x2: cx + a,
y2: cy + k * b,
x: cx + a,
y: cy,
},
PathCommand::Close,
]
}
fn shift_path_y(cmds: Vec<PathCommand>, dy: f64) -> Vec<PathCommand> {
cmds.into_iter().map(|c| match c {
PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy },
PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy },
PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo {
x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy,
},
PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo {
x1, y1: y1 + dy, x, y: y + dy,
},
PathCommand::Close => PathCommand::Close,
}).collect()
}
fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec<PathCommand> {
if let Some(commands) = crate::katex_svg::katex_stretchy_arrow_path(label, width, height) {
return commands;
}
let ah = height * 0.35; let mid_y = -height / 2.0;
match label {
"\\overleftarrow" | "\\underleftarrow" | "\\xleftarrow" | "\\xLeftarrow" => {
vec![
PathCommand::MoveTo { x: ah, y: mid_y - ah },
PathCommand::LineTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: ah, y: mid_y + ah },
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
]
}
"\\overleftrightarrow" | "\\underleftrightarrow"
| "\\xleftrightarrow" | "\\xLeftrightarrow" => {
vec![
PathCommand::MoveTo { x: ah, y: mid_y - ah },
PathCommand::LineTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: ah, y: mid_y + ah },
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::LineTo { x: width - ah, y: mid_y + ah },
]
}
"\\xlongequal" => {
let gap = 0.04;
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
PathCommand::LineTo { x: width, y: mid_y - gap },
PathCommand::MoveTo { x: 0.0, y: mid_y + gap },
PathCommand::LineTo { x: width, y: mid_y + gap },
]
}
"\\xhookleftarrow" => {
vec![
PathCommand::MoveTo { x: ah, y: mid_y - ah },
PathCommand::LineTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: ah, y: mid_y + ah },
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah },
]
}
"\\xhookrightarrow" => {
vec![
PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah },
PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::LineTo { x: width - ah, y: mid_y + ah },
]
}
"\\xrightharpoonup" | "\\xleftharpoonup" => {
let right = label.contains("right");
if right {
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
PathCommand::LineTo { x: width, y: mid_y },
]
} else {
vec![
PathCommand::MoveTo { x: ah, y: mid_y - ah },
PathCommand::LineTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
]
}
}
"\\xrightharpoondown" | "\\xleftharpoondown" => {
let right = label.contains("right");
if right {
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::MoveTo { x: width - ah, y: mid_y + ah },
PathCommand::LineTo { x: width, y: mid_y },
]
} else {
vec![
PathCommand::MoveTo { x: ah, y: mid_y + ah },
PathCommand::LineTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
]
}
}
"\\xrightleftharpoons" | "\\xleftrightharpoons" => {
let gap = 0.06;
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
PathCommand::LineTo { x: width, y: mid_y - gap },
PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
PathCommand::LineTo { x: width, y: mid_y - gap },
PathCommand::MoveTo { x: width, y: mid_y + gap },
PathCommand::LineTo { x: 0.0, y: mid_y + gap },
PathCommand::MoveTo { x: ah, y: mid_y + gap + ah },
PathCommand::LineTo { x: 0.0, y: mid_y + gap },
]
}
"\\xtofrom" | "\\xrightleftarrows" => {
let gap = 0.06;
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y - gap },
PathCommand::LineTo { x: width, y: mid_y - gap },
PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah },
PathCommand::LineTo { x: width, y: mid_y - gap },
PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah },
PathCommand::MoveTo { x: width, y: mid_y + gap },
PathCommand::LineTo { x: 0.0, y: mid_y + gap },
PathCommand::MoveTo { x: ah, y: mid_y + gap - ah },
PathCommand::LineTo { x: 0.0, y: mid_y + gap },
PathCommand::LineTo { x: ah, y: mid_y + gap + ah },
]
}
"\\overlinesegment" | "\\underlinesegment" => {
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
]
}
_ => {
vec![
PathCommand::MoveTo { x: 0.0, y: mid_y },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::MoveTo { x: width - ah, y: mid_y - ah },
PathCommand::LineTo { x: width, y: mid_y },
PathCommand::LineTo { x: width - ah, y: mid_y + ah },
]
}
}
}
fn cd_wrap_hpad(inner: LayoutBox, pad_l: f64, pad_r: f64, color: Color) -> LayoutBox {
let h = inner.height;
let d = inner.depth;
let w = inner.width + pad_l + pad_r;
let mut children: Vec<LayoutBox> = Vec::with_capacity(3);
if pad_l > 0.0 {
children.push(LayoutBox::new_kern(pad_l));
}
children.push(inner);
if pad_r > 0.0 {
children.push(LayoutBox::new_kern(pad_r));
}
LayoutBox {
width: w,
height: h,
depth: d,
content: BoxContent::HBox(children),
color,
}
}
fn cd_vcenter_side_label(label: LayoutBox, box_h: f64, box_d: f64, color: Color) -> LayoutBox {
let shift = (box_h - box_d + label.depth - label.height) / 2.0;
LayoutBox {
width: label.width,
height: box_h,
depth: box_d,
content: BoxContent::RaiseBox {
body: Box::new(label),
shift,
},
color,
}
}
fn cd_side_label_scaled(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
let sup_style = options.style.superscript();
let sup_opts = options.with_style(sup_style);
let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
let inner = layout_node(body, &sup_opts);
if (sup_ratio - 1.0).abs() < 1e-6 {
inner
} else {
LayoutBox {
width: inner.width * sup_ratio,
height: inner.height * sup_ratio,
depth: inner.depth * sup_ratio,
content: BoxContent::Scaled {
body: Box::new(inner),
child_scale: sup_ratio,
},
color: options.color,
}
}
}
fn cd_stretch_vert_arrow_box(total_height: f64, down: bool, options: &LayoutOptions) -> LayoutBox {
let axis = options.metrics().axis_height;
let depth = (total_height / 2.0 - axis).max(0.0);
let height = total_height - depth;
if let Some((commands, w)) =
crate::katex_svg::katex_cd_vert_arrow_from_rightarrow(down, total_height, axis)
{
return LayoutBox {
width: w,
height,
depth,
content: BoxContent::SvgPath {
commands,
fill: true,
},
color: options.color,
};
}
if down {
make_stretchy_delim("\\downarrow", SIZE_TO_MAX_HEIGHT[2], options)
} else {
make_stretchy_delim("\\uparrow", SIZE_TO_MAX_HEIGHT[2], options)
}
}
fn layout_cd_arrow(
direction: &str,
label_above: Option<&ParseNode>,
label_below: Option<&ParseNode>,
target_size: f64,
target_col_width: f64,
_target_depth: f64,
options: &LayoutOptions,
) -> LayoutBox {
let metrics = options.metrics();
let axis = metrics.axis_height;
const CD_VERT_SIDE_KERN_EM: f64 = 0.11;
match direction {
"right" | "left" | "horiz_eq" => {
let sup_style = options.style.superscript();
let sub_style = options.style.subscript();
let sup_opts = options.with_style(sup_style);
let sub_opts = options.with_style(sub_style);
let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier();
let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier();
let above_box = label_above.map(|n| layout_node(n, &sup_opts));
let below_box = label_below.map(|n| layout_node(n, &sub_opts));
let above_w = above_box.as_ref().map(|b| b.width * sup_ratio).unwrap_or(0.0);
let below_w = below_box.as_ref().map(|b| b.width * sub_ratio).unwrap_or(0.0);
let path_label = if direction == "right" {
"\\cdrightarrow"
} else if direction == "left" {
"\\cdleftarrow"
} else {
"\\cdlongequal"
};
let min_shaft_w = crate::katex_svg::katex_stretchy_min_width_em(path_label).unwrap_or(1.0);
const CD_LABEL_PAD_L: f64 = 0.22;
const CD_LABEL_PAD_R: f64 = 0.48;
let cd_pad_sup = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sup_ratio;
let cd_pad_sub = (CD_LABEL_PAD_L + CD_LABEL_PAD_R) * sub_ratio;
let upper_need = above_box
.as_ref()
.map(|_| above_w + cd_pad_sup)
.unwrap_or(0.0);
let lower_need = below_box
.as_ref()
.map(|_| below_w + cd_pad_sub)
.unwrap_or(0.0);
let natural_w = upper_need.max(lower_need).max(0.0);
let shaft_w = if target_size > 0.0 {
target_size
} else {
natural_w.max(min_shaft_w)
};
let (commands, actual_arrow_h, fill_arrow) =
match crate::katex_svg::katex_stretchy_path(path_label, shaft_w) {
Some((c, h)) => (c, h, true),
None => {
let arrow_h = 0.3_f64;
let ah = 0.12_f64;
let cmds = if direction == "horiz_eq" {
let gap = 0.06;
vec![
PathCommand::MoveTo { x: 0.0, y: -gap },
PathCommand::LineTo { x: shaft_w, y: -gap },
PathCommand::MoveTo { x: 0.0, y: gap },
PathCommand::LineTo { x: shaft_w, y: gap },
]
} else if direction == "right" {
vec![
PathCommand::MoveTo { x: 0.0, y: 0.0 },
PathCommand::LineTo { x: shaft_w, y: 0.0 },
PathCommand::MoveTo { x: shaft_w - ah, y: -ah },
PathCommand::LineTo { x: shaft_w, y: 0.0 },
PathCommand::LineTo { x: shaft_w - ah, y: ah },
]
} else {
vec![
PathCommand::MoveTo { x: shaft_w, y: 0.0 },
PathCommand::LineTo { x: 0.0, y: 0.0 },
PathCommand::MoveTo { x: ah, y: -ah },
PathCommand::LineTo { x: 0.0, y: 0.0 },
PathCommand::LineTo { x: ah, y: ah },
]
};
(cmds, arrow_h, false)
}
};
let arrow_half = actual_arrow_h / 2.0;
let arrow_box = LayoutBox {
width: shaft_w,
height: arrow_half,
depth: arrow_half,
content: BoxContent::SvgPath {
commands,
fill: fill_arrow,
},
color: options.color,
};
let gap = 0.111;
let sup_h = above_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0);
let sup_d = above_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0);
let sup_d_contrib = if above_box.as_ref().map(|b| b.depth).unwrap_or(0.0) > 0.25 {
sup_d
} else {
0.0
};
let height = axis + arrow_half + gap + sup_h + sup_d_contrib;
let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0);
let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0);
let depth = if below_box.is_some() {
(arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw
} else {
(arrow_half - axis).max(0.0)
};
let inner = LayoutBox {
width: shaft_w,
height,
depth,
content: BoxContent::OpLimits {
base: Box::new(arrow_box),
sup: above_box.map(Box::new),
sub: below_box.map(Box::new),
base_shift: -axis,
sup_kern: gap,
sub_kern: gap,
slant: 0.0,
sup_scale: sup_ratio,
sub_scale: sub_ratio,
},
color: options.color,
};
if target_col_width > inner.width + 1e-6 {
let extra = target_col_width - inner.width;
let kl = extra / 2.0;
let kr = extra - kl;
cd_wrap_hpad(inner, kl, kr, options.color)
} else {
inner
}
}
"down" | "up" | "vert_eq" => {
let big_total = SIZE_TO_MAX_HEIGHT[2];
let shaft_box = match direction {
"vert_eq" if target_size > 0.0 => {
make_vert_delim_box(target_size.max(big_total), true, options)
}
"vert_eq" => make_stretchy_delim("\\Vert", big_total, options),
"down" if target_size > 0.0 => {
cd_stretch_vert_arrow_box(target_size.max(1.0), true, options)
}
"up" if target_size > 0.0 => {
cd_stretch_vert_arrow_box(target_size.max(1.0), false, options)
}
"down" => cd_stretch_vert_arrow_box(big_total, true, options),
"up" => cd_stretch_vert_arrow_box(big_total, false, options),
_ => cd_stretch_vert_arrow_box(big_total, true, options),
};
let box_h = shaft_box.height;
let box_d = shaft_box.depth;
let shaft_w = shaft_box.width;
let left_box = label_above.map(|n| {
cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
});
let right_box = label_below.map(|n| {
cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color)
});
let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0);
let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0);
let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 };
let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w;
let inner_w = left_part + shaft_w + right_part;
let (kern_left, kern_right, total_w) = if target_col_width > inner_w {
let extra = target_col_width - inner_w;
let kl = extra / 2.0;
let kr = extra - kl;
(kl, kr, target_col_width)
} else {
(0.0, 0.0, inner_w)
};
let mut children: Vec<LayoutBox> = Vec::new();
if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); }
if let Some(lb) = left_box {
children.push(lb);
children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
}
children.push(shaft_box);
if let Some(rb) = right_box {
children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM));
children.push(rb);
}
if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); }
LayoutBox {
width: total_w,
height: box_h,
depth: box_d,
content: BoxContent::HBox(children),
color: options.color,
}
}
_ => LayoutBox::new_empty(),
}
}
fn layout_cd(body: &[Vec<ParseNode>], options: &LayoutOptions) -> LayoutBox {
let metrics = options.metrics();
let pt = 1.0 / metrics.pt_per_em;
let baselineskip = 3.0 * metrics.x_height;
let arstrut_h = 0.7 * baselineskip;
let arstrut_d = 0.3 * baselineskip;
let num_rows = body.len();
if num_rows == 0 {
return LayoutBox::new_empty();
}
let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
if num_cols == 0 {
return LayoutBox::new_empty();
}
let jot = 3.0 * pt;
let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(num_rows);
let mut col_widths = vec![0.0_f64; num_cols];
let mut row_heights = vec![arstrut_h; num_rows];
let mut row_depths = vec![arstrut_d; num_rows];
for (r, row) in body.iter().enumerate() {
let mut row_boxes: Vec<LayoutBox> = Vec::with_capacity(num_cols);
for (c, cell) in row.iter().enumerate() {
let cbox = match cell {
ParseNode::CdArrow { direction, label_above, label_below, .. } => {
layout_cd_arrow(
direction,
label_above.as_deref(),
label_below.as_deref(),
0.0, 0.0, 0.0, options,
)
}
ParseNode::OrdGroup { body: cell_body, .. } => {
layout_expression(cell_body, options, true)
}
other => layout_node(other, options),
};
row_heights[r] = row_heights[r].max(cbox.height);
row_depths[r] = row_depths[r].max(cbox.depth);
col_widths[c] = col_widths[c].max(cbox.width);
row_boxes.push(cbox);
}
while row_boxes.len() < num_cols {
row_boxes.push(LayoutBox::new_empty());
}
cell_boxes.push(row_boxes);
}
let col_target_w: Vec<f64> = col_widths.clone();
#[cfg(debug_assertions)]
{
eprintln!("[CD] pass1 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
for (r, row) in cell_boxes.iter().enumerate() {
for (c, b) in row.iter().enumerate() {
if b.width > 0.0 {
eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth);
}
}
}
}
for (r, row) in body.iter().enumerate() {
let is_arrow_row = r % 2 == 1;
for (c, cell) in row.iter().enumerate() {
if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell {
let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq");
let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz {
let b = layout_cd_arrow(
direction,
label_above.as_deref(),
label_below.as_deref(),
cell_boxes[r][c].width,
col_target_w[c],
0.0,
options,
);
let w = b.width;
(b, w)
} else if is_arrow_row && c % 2 == 0 {
let v_span = row_heights[r] + row_depths[r];
let b = layout_cd_arrow(
direction,
label_above.as_deref(),
label_below.as_deref(),
v_span,
col_widths[c],
0.0,
options,
);
let w = b.width;
(b, w)
} else {
continue;
};
col_widths[c] = col_widths[c].max(col_w);
cell_boxes[r][c] = new_box;
}
}
}
#[cfg(debug_assertions)]
{
eprintln!("[CD] pass2 col_widths={col_widths:?} row_heights={row_heights:?} row_depths={row_depths:?}");
}
for rd in &mut row_depths {
*rd += jot;
}
let col_gap = 0.5;
let col_aligns: Vec<u8> = (0..num_cols).map(|_| b'c').collect();
let col_separators = vec![None; num_cols + 1];
let mut total_height = 0.0_f64;
let mut row_positions = Vec::with_capacity(num_rows);
for r in 0..num_rows {
total_height += row_heights[r];
row_positions.push(total_height);
total_height += row_depths[r];
}
let offset = total_height / 2.0 + metrics.axis_height;
let height = offset;
let depth = total_height - offset;
let total_width = col_widths.iter().sum::<f64>()
+ col_gap * (num_cols.saturating_sub(1)) as f64;
let hlines_before_row: Vec<Vec<bool>> = (0..=num_rows).map(|_| vec![]).collect();
LayoutBox {
width: total_width,
height,
depth,
content: BoxContent::Array {
cells: cell_boxes,
col_widths,
col_aligns,
row_heights,
row_depths,
col_gap,
offset,
content_x_offset: 0.0,
col_separators,
hlines_before_row,
rule_thickness: 0.04 * pt,
double_rule_sep: metrics.double_rule_sep,
},
color: options.color,
}
}
fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec<PathCommand> {
let mid = width / 2.0;
let q = height * 0.6;
if is_over {
vec![
PathCommand::MoveTo { x: 0.0, y: 0.0 },
PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q },
PathCommand::LineTo { x: mid - 0.05, y: -q },
PathCommand::LineTo { x: mid, y: -height },
PathCommand::LineTo { x: mid + 0.05, y: -q },
PathCommand::LineTo { x: width - mid * 0.4, y: -q },
PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 },
]
} else {
vec![
PathCommand::MoveTo { x: 0.0, y: 0.0 },
PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q },
PathCommand::LineTo { x: mid - 0.05, y: q },
PathCommand::LineTo { x: mid, y: height },
PathCommand::LineTo { x: mid + 0.05, y: q },
PathCommand::LineTo { x: width - mid * 0.4, y: q },
PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 },
]
}
}