pub mod alphabet;
mod arithmetic;
pub mod math_box;
mod stretch;
mod table;
pub use alphabet::math_italic;
pub use math_box::{Item, MathBox};
use ttf_parser::math::Constants;
use crate::font::{Font, MathFont};
use crate::locale::Locale;
use crate::rules::{self, Registry};
use crate::tree::*;
use crate::Error;
type R<T> = Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Level {
Display,
Text,
Script,
ScriptScript,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Style {
pub level: Level,
pub cramped: bool,
pub font: MathFont,
}
impl Style {
pub fn display() -> Style {
Style { level: Level::Display, cramped: false, font: MathFont::default() }
}
pub fn text() -> Style {
Style { level: Level::Text, cramped: false, font: MathFont::default() }
}
pub fn in_font(self, font: MathFont) -> Style {
Style { font, ..self }
}
pub fn font(self) -> &'static Font {
self.font.get()
}
pub fn scale(self) -> f32 {
match self.level {
Level::Display | Level::Text => 1.0,
Level::Script => self.font().script_scale(),
Level::ScriptScript => self.font().script_script_scale(),
}
}
pub fn is_script(self) -> bool {
matches!(self.level, Level::Script | Level::ScriptScript)
}
fn sup(self) -> Style {
let level = match self.level {
Level::Display | Level::Text => Level::Script,
_ => Level::ScriptScript,
};
Style { level, cramped: self.cramped, font: self.font }
}
fn sub(self) -> Style {
Style { cramped: true, ..self.sup() }
}
fn num(self) -> Style {
let level = match self.level {
Level::Display => Level::Text,
Level::Text => Level::Script,
_ => Level::ScriptScript,
};
Style { level, cramped: self.cramped, font: self.font }
}
fn den(self) -> Style {
Style { cramped: true, ..self.num() }
}
fn cramp(self) -> Style {
Style { cramped: true, ..self }
}
fn not_display(self) -> Style {
if self.level == Level::Display {
Style { level: Level::Text, ..self }
} else {
self
}
}
}
pub struct Context<'a> {
pub locale: Locale,
pub registry: &'a mut Registry,
pub strict: bool,
}
fn k(e: Style, f: impl Fn(&Constants<'static>) -> ttf_parser::math::MathValue<'static>) -> f32 {
e.font().c(f) * e.scale()
}
fn em(e: Style, amount: f32) -> f32 {
e.font().units_per_em() * amount * e.scale()
}
pub(crate) fn glyph(c: char, e: Style) -> R<MathBox> {
let f = e.font();
let id = f.glyph(c).or_else(|| equivalent(c).and_then(|x| f.glyph(x))).ok_or(Error::MissingGlyph { character: c })?;
Ok(glyph_id(id, e.scale(), f))
}
fn equivalent(c: char) -> Option<char> {
Some(match c {
'✔' | '✅' => '✓',
'✗' | '✘' | '❌' => '⨯',
_ => return None,
})
}
pub(crate) fn glyph_id(id: ttf_parser::GlyphId, s: f32, f: &Font) -> MathBox {
let (_, y0, _, y1) = f.limits(id);
MathBox {
width: f.advance(id) * s,
height: y1.max(0.0) * s,
depth: (-y0).max(0.0) * s,
items: vec![Item::Glyph { id, x: 0.0, y: 0.0, scale: s, color: None }],
italic: f.italic_correction(id) * s,
accent_point: f.accent_point(id).map(|v| v * s),
single_glyph: true,
}
}
const SMALL_CAPS_SCALE: f32 = 0.76;
fn upright_text(t: &str, e: Style, ctx: &mut Context) -> R<MathBox> {
use crate::input::latex::{BOLD_END, BOLD_START, SMALL_CAPS_END, SMALL_CAPS_START};
let mut math_box = MathBox::empty();
let mut bold = 0;
let mut small_caps = 0;
for c in t.chars() {
match c {
BOLD_START => bold += 1,
BOLD_END => bold -= 1,
SMALL_CAPS_START => small_caps += 1,
SMALL_CAPS_END => small_caps -= 1,
_ => {}
}
if matches!(c, BOLD_START | BOLD_END | SMALL_CAPS_START | SMALL_CAPS_END) {
continue;
}
if small_caps > 0 && c.is_lowercase() {
if let Some(id) = c.to_uppercase().next().and_then(|u| e.font().glyph(u)) {
math_box.append(&glyph_id(id, e.scale() * SMALL_CAPS_SCALE, e.font()));
continue;
}
}
let c = if bold > 0 { alphabet::character(c, Variant::Bold) } else { c };
if c == ' ' || c == '\u{00A0}' {
math_box.append(&MathBox::space(em(e, e.font.word_space())));
continue;
}
if c == '\u{2003}' {
math_box.append(&MathBox::space(em(e, 1.0)));
continue;
}
if c == '\u{200B}' {
continue;
}
match glyph(c, e) {
Ok(g) => math_box.append(&g),
Err(Error::MissingGlyph { character }) if !ctx.strict => {
ctx.registry.warn(rules::E_REC_8, format!("the font has no '{character}' (U+{:04X}): drew a box", character as u32));
math_box.append(&glyph('□', e)?);
}
Err(error) => return Err(error),
}
}
math_box.single_glyph = false;
Ok(math_box)
}
pub fn compose(node: &Node, e: Style, ctx: &mut Context) -> R<MathBox> {
let leaf = matches!(node, Node::Identifier { .. } | Node::Operator { .. } | Node::Number { .. } | Node::FixedDelimiter { .. } | Node::LargeOperator { .. });
match compose_node(node, e, ctx) {
Err(Error::MissingGlyph { character }) if leaf && !ctx.strict => {
ctx.registry.warn(rules::E_REC_8, format!("the font has no '{character}' (U+{:04X}): drew a box", character as u32));
glyph('□', e)
}
other => other,
}
}
fn compose_node(node: &Node, e: Style, ctx: &mut Context) -> R<MathBox> {
match node {
Node::Number { value, group, thousands } => compose_number(value, *group, *thousands, e, ctx),
Node::Identifier { name, variant } => {
let mut math_box = MathBox::empty();
for c in name.chars() {
let mapped = alphabet::character(c, *variant);
let g = glyph(mapped, e).or_else(|_| glyph(c, e))?;
if name.chars().count() == 1 {
return Ok(g);
}
math_box.append(&g);
}
Ok(math_box)
}
Node::Function { name, .. } => {
let local = ctx.locale.function_name(name);
if local != *name {
ctx.registry.note(rules::L_FUN_1, format!("function {name} written as '{local}' ({})", ctx.locale));
}
upright_text(&local, e, ctx)
}
Node::Operator { symbol, .. } => glyph(*symbol, e),
Node::FixedDelimiter { symbol, size, .. } => {
let total = [1.2, 1.8, 2.4, 3.0][(*size as usize).clamp(1, 4) - 1];
stretch::delimiter(*symbol, em(e, total), e)
}
Node::LargeOperator { symbol, .. } => large_operator(*symbol, e),
Node::Row { children } => compose_row(children, e, ctx, false),
Node::Fraction { num, den, style, rule } => compose_fraction(num, den, *style, *rule, e, ctx),
Node::Root { radicand, index } => compose_root(radicand, index.as_deref(), e, ctx),
Node::Scripts { base, sup, sub } => compose_scripts(base, sup.as_deref(), sub.as_deref(), e, ctx),
Node::Delimited { open, close, parts, middles } => compose_delimited(*open, *close, parts, middles, e, ctx),
Node::Table { shape: kind, rows, columns, verticals, horizontals, between, intertexts } => {
table::compose(*kind, rows, columns, verticals, horizontals, between, intertexts, e, ctx)
}
Node::Multicolumn { base, .. } => compose(base, e, ctx),
Node::Raised { em: amount, base } => {
let mut math_box = compose(base, e, ctx)?;
math_box.shift_vertical(em(e, *amount));
math_box.single_glyph = false;
Ok(math_box)
}
Node::Scaled { factor, base } => {
let mut math_box = compose(base, e, ctx)?;
math_box.scale_box(*factor);
Ok(math_box)
}
Node::Rule { width, height, raise } => Ok(MathBox::bar(em(e, *width), em(e, *height), em(e, *raise))),
Node::Smashed { base, height, depth } => {
let mut math_box = compose(base, e, ctx)?;
if *height {
math_box.height = 0.0;
}
if *depth {
math_box.depth = 0.0;
}
math_box.single_glyph = false;
Ok(math_box)
}
Node::Overlapped { base, side } => {
let b = compose(base, e, ctx)?;
let dx = match side {
'l' => -b.width,
'c' => -b.width / 2.0,
_ => 0.0,
};
let mut math_box = MathBox::empty();
math_box.place(&b, dx, 0.0);
Ok(math_box)
}
Node::VerticalArrow { symbol, left, right } => compose_vertical_arrow(*symbol, left.as_deref(), right.as_deref(), e, ctx),
Node::Accent { base, accent, stretchy, below } => compose_accent(base, *accent, *stretchy, *below, e, ctx),
Node::Bar { base, above } => compose_bar(base, *above, e, ctx),
Node::Brace { base, above, label, bracket } => compose_brace(base, *above, label.as_deref(), *bracket, e, ctx),
Node::Text { parts } => {
let mut math_box = MathBox::empty();
for p in parts {
match p {
TextPart::Text(t) => math_box.append(&upright_text(t, e, ctx)?),
TextPart::Math(m) => math_box.append(&compose(m, e.not_display(), ctx)?),
}
}
math_box.single_glyph = false;
Ok(math_box)
}
Node::Space { em: amount } => Ok(MathBox::space(em(e, *amount))),
Node::Canceled { base, shape: kind } => compose_canceled(base, *kind, e, ctx),
Node::Framed { base, shape, background, border } => compose_frame(base, *shape, background.as_deref(), border.as_deref(), e, ctx),
Node::Colored { color, base } => {
let mut math_box = compose(base, e, ctx)?;
math_box.colorize(color);
Ok(math_box)
}
Node::Quantity { unit, .. } => compose(unit, e, ctx),
Node::Arrow { symbol, above, below } => compose_arrow(*symbol, above.as_deref(), below.as_deref(), e, ctx),
Node::Stacked { base, above, below } => {
let b = compose(base, e, ctx)?;
limits(b, above.as_deref(), below.as_deref(), e, ctx)
}
Node::Phantom { base, width, height } => {
let mut math_box = compose(base, e, ctx)?;
math_box.items.clear();
if !width {
math_box.width = 0.0;
}
if !height {
math_box.height = 0.0;
math_box.depth = 0.0;
}
Ok(math_box)
}
Node::ForcedClass { base, .. } => compose(base, e, ctx),
Node::Style { level, base } => {
let level = match level {
0 => Level::Display,
1 => Level::Text,
2 => Level::Script,
_ => Level::ScriptScript,
};
compose(base, Style { level, cramped: e.cramped, font: e.font }, ctx)
}
Node::LongDivision { dividend, divisor } => arithmetic::long_division(dividend, divisor, e),
Node::ColumnArithmetic { operation, terms } => arithmetic::column_arithmetic(*operation, terms, e),
}
}
fn compose_number(value: &str, group: bool, thousands: Option<char>, e: Style, ctx: &mut Context) -> R<MathBox> {
let sep = ctx.locale.decimal_separator();
if value.contains('.') && sep != '.' {
ctx.registry.note(rules::L_NUM_2, format!("{value} written with '{sep}' as decimal separator ({})", ctx.locale));
}
let (integer_part, fraction) = value.split_once('.').map_or((value, None), |(a, b)| (a, Some(b)));
let mut math_box = MathBox::empty();
let n = integer_part.chars().count();
for (i, c) in integer_part.chars().enumerate() {
if group && n >= 5 && i > 0 && (n - i) % 3 == 0 {
math_box.append(&MathBox::space(em(e, 3.0 / 18.0)));
}
if let Some(m) = thousands {
if i > 0 && (n - i) % 3 == 0 {
math_box.append(&glyph(m, e)?);
}
}
math_box.append(&glyph(c, e)?);
}
if let Some(fr) = fraction {
math_box.append(&glyph(sep, e)?);
for c in fr.chars() {
math_box.append(&glyph(c, e)?);
}
}
math_box.single_glyph = value.chars().count() == 1;
Ok(math_box)
}
pub fn class_of(node: &Node) -> Option<Class> {
Some(match node {
Node::Space { .. } => return None,
Node::Function { .. } | Node::LargeOperator { .. } => Class::Op,
Node::Operator { class, .. } | Node::FixedDelimiter { class, .. } | Node::ForcedClass { class, .. } => *class,
Node::Fraction { .. } | Node::Delimited { .. } => Class::Inner,
Node::Table { shape: kind, .. } if !matches!(kind, TableKind::Array | TableKind::Aligned | TableKind::Centered | TableKind::Substack | TableKind::Diagram) => Class::Inner,
Node::Multicolumn { base, .. } | Node::Raised { base, .. } | Node::Scaled { base, .. } | Node::Smashed { base, .. } => return class_of(base),
Node::Scripts { base, .. } => return class_of(base),
Node::Arrow { .. } => Class::Rel,
Node::Colored { base, .. } | Node::Style { base, .. } => match base.as_ref() {
Node::Row { children } if children.len() == 1 => return class_of(&children[0]),
Node::Row { .. } => Class::Ord,
other => return class_of(other),
},
_ => Class::Ord,
})
}
pub fn classes(children: &[Node], locale: Locale, mut registry: Option<&mut Registry>, initial_ord: bool) -> Vec<Option<Class>> {
let mut cl: Vec<Option<Class>> = children.iter().map(class_of).collect();
let neighbor_before =
|cl: &[Option<Class>], i: usize| -> Option<Class> { cl[..i].iter().rev().flatten().next().copied().or(if initial_ord { Some(Class::Ord) } else { None }) };
let neighbor_after = |cl: &[Option<Class>], i: usize| -> Option<Class> { cl[i + 1..].iter().flatten().next().copied() };
if locale.open_interval() {
for i in 0..children.len() {
let Node::Operator { symbol, .. } = &children[i] else {
continue;
};
let before = neighbor_before(&cl, i);
let after = neighbor_after(&cl, i);
let no_operand = matches!(before, None | Some(Class::Bin | Class::Op | Class::Rel | Class::Open | Class::Punct));
if *symbol == ']' && no_operand && after.is_some() {
cl[i] = Some(Class::Open);
if let Some(r) = registry.as_deref_mut() {
r.note(rules::L_INT_1, "']' at the start read as an interval opening");
}
} else if *symbol == '['
&& matches!(before, Some(Class::Ord | Class::Close | Class::Inner))
&& matches!(after, None | Some(Class::Rel | Class::Punct | Class::Close | Class::Bin))
&& children[..i].iter().any(|n| matches!(n, Node::Operator { symbol: ']', .. }))
{
cl[i] = Some(Class::Close);
if let Some(r) = registry.as_deref_mut() {
r.note(rules::L_INT_1, "'[' at the end read as an interval closing");
}
}
}
}
for i in 0..cl.len() {
if cl[i] != Some(Class::Bin) {
continue;
}
let before = neighbor_before(&cl, i);
let after = neighbor_after(&cl, i);
let no_left = matches!(before, None | Some(Class::Bin | Class::Op | Class::Rel | Class::Open | Class::Punct));
let no_right = matches!(after, None | Some(Class::Rel | Class::Close | Class::Punct));
if no_left || no_right {
cl[i] = Some(Class::Ord);
if let (Some(r), Node::Operator { symbol, .. }) = (registry.as_deref_mut(), &children[i]) {
r.note(rules::L_ESP_2, format!("'{symbol}' is unary: no operator spacing"));
}
}
}
cl
}
fn mu_space(left: Class, right: Class, index: bool) -> f32 {
use Class::*;
let idx = |c: Class| match c {
Ord => 0,
Op => 1,
Bin => 2,
Rel => 3,
Open => 4,
Close => 5,
Punct => 6,
Inner => 7,
};
const T: [[i8; 8]; 8] = [
[0, 1, -2, -3, 0, 0, 0, -1], [1, 1, 0, -3, 0, 0, 0, -1], [-2, -2, 0, 0, -2, 0, 0, -2], [-3, -3, 0, 0, -3, 0, 0, -3], [0, 0, 0, 0, 0, 0, 0, 0], [0, 1, -2, -3, 0, 0, 0, -1], [-1, -1, 0, -1, -1, -1, -1, -1], [-1, 1, -2, -3, -1, 0, -1, -1], ];
let v = T[idx(left)][idx(right)];
if v < 0 && index {
return 0.0;
}
match v.abs() {
1 => 3.0,
2 => 4.0,
3 => 5.0,
_ => 0.0,
}
}
pub(crate) fn compose_row(children: &[Node], e: Style, ctx: &mut Context, initial_ord: bool) -> R<MathBox> {
if let ([single], false) = (children, initial_ord) {
return compose(single, e, ctx);
}
let cl = classes(children, ctx.locale, Some(ctx.registry), initial_ord);
let mu = em(e, 1.0 / 18.0);
let mut math_box = MathBox::empty();
let mut previous = if initial_ord { Some(Class::Ord) } else { None };
let mut pending_italic = 0.0;
for (node, class) in children.iter().zip(&cl) {
let b = compose(node, e, ctx)?;
if let (Some(a), Some(c)) = (previous, class) {
math_box.append(&MathBox::space(mu_space(a, *c, e.is_script()) * mu));
}
if pending_italic > 0.0 && !matches!(node, Node::Identifier { .. }) {
math_box.append(&MathBox::space(pending_italic));
}
pending_italic = if matches!(node, Node::Identifier { variant: Variant::Italic | Variant::BoldItalic, .. }) { b.italic } else { 0.0 };
math_box.append(&b);
if class.is_some() {
previous = *class;
}
}
math_box.single_glyph = false;
Ok(math_box)
}
fn compose_cell(node: &Node, e: Style, ctx: &mut Context, initial_ord: bool) -> R<MathBox> {
match node {
Node::Row { children } => compose_row(children, e, ctx, initial_ord),
other => compose_row(std::slice::from_ref(other), e, ctx, initial_ord),
}
}
fn compose_fraction(num: &Node, den: &Node, style: FractionStyle, rule: bool, e: Style, ctx: &mut Context) -> R<MathBox> {
let e = match style {
FractionStyle::Display => Style { level: Level::Display, ..e },
FractionStyle::Row => e.not_display(),
FractionStyle::Auto => e,
};
let n = compose(num, e.num(), ctx)?;
let d = compose(den, e.den(), ctx)?;
let display = e.level == Level::Display;
let axis = k(e, |c| c.axis_height());
let (mut u, mut v);
let mut thickness = 0.0;
if rule {
thickness = k(e, |c| c.fraction_rule_thickness());
u = if display { k(e, |c| c.fraction_numerator_display_style_shift_up()) } else { k(e, |c| c.fraction_numerator_shift_up()) };
v = if display { k(e, |c| c.fraction_denominator_display_style_shift_down()) } else { k(e, |c| c.fraction_denominator_shift_down()) };
let gap_n = if display { k(e, |c| c.fraction_num_display_style_gap_min()) } else { k(e, |c| c.fraction_numerator_gap_min()) };
let gap_d = if display { k(e, |c| c.fraction_denom_display_style_gap_min()) } else { k(e, |c| c.fraction_denominator_gap_min()) };
u = u.max(gap_n + thickness / 2.0 + axis + n.depth);
v = v.max(gap_d + thickness / 2.0 - axis + d.height);
} else {
u = if display { k(e, |c| c.stack_top_display_style_shift_up()) } else { k(e, |c| c.stack_top_shift_up()) };
v = if display { k(e, |c| c.stack_bottom_display_style_shift_down()) } else { k(e, |c| c.stack_bottom_shift_down()) };
let gap_min = if display { k(e, |c| c.stack_display_style_gap_min()) } else { k(e, |c| c.stack_gap_min()) };
let gap = (u - n.depth) - (d.height - v);
if gap < gap_min {
let delta = (gap_min - gap) / 2.0;
u += delta;
v += delta;
}
}
let margin = em(e, 0.12);
let width = n.width.max(d.width);
let mut math_box = MathBox::space(width + 2.0 * margin);
math_box.place(&n, margin + (width - n.width) / 2.0, u);
math_box.place(&d, margin + (width - d.width) / 2.0, -v);
if rule {
math_box.place(&MathBox::bar(width, thickness, axis - thickness / 2.0), margin, 0.0);
}
Ok(math_box)
}
fn compose_root(radicand: &Node, index: Option<&Node>, e: Style, ctx: &mut Context) -> R<MathBox> {
let f = e.font();
let base = compose(radicand, e.cramp(), ctx)?;
let thickness = k(e, |c| c.radical_rule_thickness());
let mut gap = if e.level == Level::Display { k(e, |c| c.radical_display_style_vertical_gap()) } else { k(e, |c| c.radical_vertical_gap()) };
let extra = k(e, |c| c.radical_extra_ascender());
let target = base.height + base.depth + gap + thickness;
let mut radical = stretch::vertical('√', target, e)?;
let total = radical.total();
if total > target {
gap += (total - target) / 2.0;
}
let top = base.height + gap + thickness;
let dy = top - radical.height;
radical.shift_vertical(dy);
let mut math_box = MathBox::empty();
if let Some(ind) = index {
let ei = Style { level: Level::ScriptScript, cramped: true, font: e.font };
let i = compose(ind, ei, ctx)?;
let before = k(e, |c| c.radical_kern_before_degree());
let after = k(e, |c| c.radical_kern_after_degree());
let raise = f.radical_degree_raise() * radical.total() - radical.depth;
math_box.place(&i, before, raise + i.depth);
math_box.width = (before + i.width + after).max(0.0);
}
let x_radical = math_box.width;
math_box.place(&radical, x_radical, 0.0);
let x_base = x_radical + radical.width;
math_box.place(&base, x_base, 0.0);
math_box.place(&MathBox::bar(base.width, thickness, top - thickness), x_base, 0.0);
math_box.width = x_base + base.width;
math_box.height = math_box.height.max(top + extra);
Ok(math_box)
}
fn uses_limits(base: &Node, e: Style) -> bool {
let display = e.level == Level::Display;
match base {
Node::LargeOperator { symbol, limits } => {
let by_default = !is_integral(*symbol);
limits.unwrap_or(by_default && display)
}
Node::Function { limits, .. } => *limits && display,
Node::ForcedClass { base, class: Class::Op } => uses_limits(base, e) || display,
_ => false,
}
}
pub(crate) fn is_integral(c: char) -> bool {
matches!(c, '∫' | '∬' | '∭' | '⨌' | '∮' | '∯' | '∰' | '∱' | '∲' | '∳' | '⨏' | '⨖')
}
fn compose_scripts(base: &Node, sup: Option<&Node>, sub: Option<&Node>, e: Style, ctx: &mut Context) -> R<MathBox> {
let b = compose(base, e, ctx)?;
if uses_limits(base, e) {
return limits(b, sup, sub, e, ctx);
}
let integral = matches!(base, Node::LargeOperator { .. });
let (primes, sup) = split_primes(sup);
let mut b = b;
if let Some(p) = primes {
let mut with_primes = b.clone();
with_primes.append(&MathBox::space(b.italic));
with_primes.append(&compose(&p, e, ctx)?);
with_primes.single_glyph = false;
with_primes.italic = 0.0;
b = with_primes;
if sup.is_none() && sub.is_none() {
return Ok(b);
}
}
let is_simple = b.single_glyph && !integral;
let s = sup.as_ref().map(|n| compose(n, e.sup(), ctx)).transpose()?;
let d = sub.map(|n| compose(n, e.sub(), ctx)).transpose()?;
let mut u = 0.0;
let mut v = 0.0;
if let Some(s) = &s {
let rise = if e.cramped { k(e, |c| c.superscript_shift_up_cramped()) } else { k(e, |c| c.superscript_shift_up()) };
let drop = if is_simple { 0.0 } else { b.height - k(e, |c| c.superscript_baseline_drop_max()) };
u = rise.max(drop).max(s.depth + k(e, |c| c.superscript_bottom_min()));
}
if let Some(d) = &d {
let descent = k(e, |c| c.subscript_shift_down());
let drop = if is_simple { 0.0 } else { b.depth + k(e, |c| c.subscript_baseline_drop_min()) };
v = descent.max(drop).max(d.height - k(e, |c| c.subscript_top_max()));
}
if let (Some(s), Some(d)) = (&s, &d) {
let gap = (u - s.depth) - (d.height - v);
let gap_min = k(e, |c| c.sub_superscript_gap_min());
if gap < gap_min {
v += gap_min - gap;
}
let raise_by = k(e, |c| c.superscript_bottom_max_with_subscript()) - (u - s.depth);
if raise_by > 0.0 {
u += raise_by;
v -= raise_by;
}
}
let delta = b.italic;
let (x_sup, x_sub) = if integral { (b.width + delta * 0.5, b.width - delta * 0.5) } else { (b.width + delta, b.width) };
let mut math_box = b.clone();
math_box.single_glyph = false;
let mut width = b.width;
if let Some(s) = &s {
math_box.place(s, x_sup, u);
width = width.max(x_sup + s.width);
}
if let Some(d) = &d {
math_box.place(d, x_sub, -v);
width = width.max(x_sub + d.width);
}
math_box.width = width + k(e, |c| c.space_after_script());
math_box.italic = 0.0;
Ok(math_box)
}
fn split_primes(sup: Option<&Node>) -> (Option<Node>, Option<Node>) {
let prime = |n: &Node| matches!(n, Node::Operator { symbol: '′' | '″' | '‴', .. });
match sup {
Some(n) if prime(n) => (Some(n.clone()), None),
Some(Node::Row { children }) if children.first().is_some_and(prime) => {
let qty = children.iter().take_while(|f| prime(f)).count();
let remainder = &children[qty..];
let remainder = if remainder.is_empty() { None } else { Some(Node::row(remainder.to_vec()).simplify()) };
(Some(Node::row(children[..qty].to_vec())), remainder)
}
other => (None, other.cloned()),
}
}
fn limits(b: MathBox, above: Option<&Node>, below: Option<&Node>, e: Style, ctx: &mut Context) -> R<MathBox> {
let s = above.map(|n| compose(n, e.sup(), ctx)).transpose()?;
let d = below.map(|n| compose(n, e.sub(), ctx)).transpose()?;
let width = b.width.max(s.as_ref().map_or(0.0, |s| s.width)).max(d.as_ref().map_or(0.0, |d| d.width));
let delta = b.italic;
let mut math_box = MathBox::space(width);
math_box.place(&b, (width - b.width) / 2.0, 0.0);
if let Some(s) = &s {
let gap = k(e, |c| c.upper_limit_gap_min()).max(k(e, |c| c.upper_limit_baseline_rise_min()) - s.depth);
math_box.place(s, (width - s.width + delta) / 2.0, b.height + gap + s.depth);
}
if let Some(d) = &d {
let gap = k(e, |c| c.lower_limit_gap_min()).max(k(e, |c| c.lower_limit_baseline_drop_min()) - d.height);
math_box.place(d, (width - d.width - delta) / 2.0, -(b.depth + gap + d.height));
}
Ok(math_box)
}
fn large_operator(c: char, e: Style) -> R<MathBox> {
let f = e.font();
let id = f.glyph(c).ok_or(Error::MissingGlyph { character: c })?;
let mut chosen = id;
if e.level == Level::Display {
let minimum = f.display_operator_min_height();
for (v, measure) in f.variants(id, true) {
chosen = v;
if measure >= minimum {
break;
}
}
}
let mut math_box = glyph_id(chosen, e.scale(), f);
let axis = k(e, |c| c.axis_height());
let offset = axis - (math_box.height - math_box.depth) / 2.0;
math_box.shift_vertical(offset);
math_box.single_glyph = true;
Ok(math_box)
}
fn compose_delimited(open: Option<char>, close: Option<char>, parts: &[Node], middles: &[char], e: Style, ctx: &mut Context) -> R<MathBox> {
let contents = parts.iter().map(|p| compose(p, e, ctx)).collect::<R<Vec<_>>>()?;
let axis = k(e, |c| c.axis_height());
let half = contents.iter().map(|c| (c.height - axis).max(c.depth + axis)).fold(0.0f32, f32::max);
let target = (2.0 * half * 0.901).max(2.0 * half - em(e, 0.5));
let delim = |c: Option<char>| -> R<MathBox> {
match c {
Some(c) => stretch::delimiter(c, target, e),
None => Ok(MathBox::space(em(e, 0.12))),
}
};
let mut math_box = delim(open)?;
for (i, content) in contents.iter().enumerate() {
if i > 0 {
math_box.append(&delim(middles.get(i - 1).copied())?);
}
math_box.append(content);
}
math_box.append(&delim(close)?);
math_box.single_glyph = false;
Ok(math_box)
}
fn combining(c: char) -> bool {
matches!(c as u32, 0x0300..=0x036F | 0x20D0..=0x20FF)
}
fn compose_accent(base: &Node, accent: char, stretchy: bool, below: bool, e: Style, ctx: &mut Context) -> R<MathBox> {
if below {
return compose_under_accent(base, accent, e, ctx);
}
let b = compose(base, e.cramp(), ctx)?;
let base_point = if b.single_glyph { b.accent_point.unwrap_or(b.width / 2.0) } else { b.width / 2.0 };
let a = if stretchy { stretch::horizontal(accent, b.width, e)? } else { glyph(accent, e)? };
let point_a = if stretchy {
a.width / 2.0
} else {
a.accent_point.unwrap_or_else(|| {
let id = e.font().glyph(accent).expect("glyph already resolved");
let (x0, _, x1, _) = e.font().limits(id);
(x0 + x1) / 2.0 * e.scale()
})
};
let dy = if combining(accent) {
let base_height = k(e, |c| c.accent_base_height());
(b.height - base_height).max(0.0)
} else {
b.height + k(e, |c| c.overbar_vertical_gap()) / 2.0 + a.depth
};
let mut math_box = b.clone();
if b.width == 0.0 && b.items.is_empty() {
let glyphs = a.items.iter().filter_map(|i| if let Item::Glyph { id, x, scale, .. } = i { Some((*id, *x, *scale)) } else { None });
let (x0, x1) = glyphs.fold((0.0f32, a.width), |(x0, x1), (id, x, scale)| {
let (g0, _, g1, _) = e.font().limits(id);
(x0.min(x + g0 * scale), x1.max(x + g1 * scale))
});
math_box.place(&a, -x0, dy);
math_box.width = x1 - x0;
} else {
math_box.place(&a, base_point - point_a, dy);
}
math_box.single_glyph = false;
math_box.italic = b.italic;
Ok(math_box)
}
fn compose_under_accent(base: &Node, accent: char, e: Style, ctx: &mut Context) -> R<MathBox> {
let b = compose(base, e, ctx)?;
let a = stretch::horizontal(accent, b.width, e)?;
let width = b.width.max(a.width);
let gap = k(e, |c| c.underbar_vertical_gap());
let mut math_box = MathBox::space(width);
math_box.place(&b, (width - b.width) / 2.0, 0.0);
math_box.place(&a, (width - a.width) / 2.0, -(b.depth + gap + a.height));
math_box.depth += k(e, |c| c.underbar_extra_descender());
math_box.italic = b.italic;
Ok(math_box)
}
fn compose_bar(base: &Node, above: bool, e: Style, ctx: &mut Context) -> R<MathBox> {
let b = compose(base, if above { e.cramp() } else { e }, ctx)?;
let mut math_box = b.clone();
math_box.single_glyph = false;
if above {
let gap = k(e, |c| c.overbar_vertical_gap());
let t = k(e, |c| c.overbar_rule_thickness());
math_box.place(&MathBox::bar(b.width, t, b.height + gap), 0.0, 0.0);
math_box.height += k(e, |c| c.overbar_extra_ascender());
} else {
let gap = k(e, |c| c.underbar_vertical_gap());
let t = k(e, |c| c.underbar_rule_thickness());
math_box.place(&MathBox::bar(b.width, t, -(b.depth + gap + t)), 0.0, 0.0);
math_box.depth += k(e, |c| c.underbar_extra_descender());
}
Ok(math_box)
}
fn compose_brace(base: &Node, above: bool, label: Option<&Node>, bracket: bool, e: Style, ctx: &mut Context) -> R<MathBox> {
let b = compose(base, e, ctx)?;
let symbol = match (above, bracket) {
(true, false) => '⏞',
(false, false) => '⏟',
(true, true) => '⎴',
(false, true) => '⎵',
};
let brace = stretch::horizontal(symbol, b.width, e)?;
let r = label.map(|n| compose(n, e.sup(), ctx)).transpose()?;
let width = b.width.max(brace.width).max(r.as_ref().map_or(0.0, |r| r.width));
let gap = k(e, |c| c.overbar_vertical_gap());
let label_gap = k(e, |c| c.stretch_stack_gap_above_min());
let mut math_box = MathBox::space(width);
math_box.place(&b, (width - b.width) / 2.0, 0.0);
if above {
let y = b.height + gap + brace.depth;
math_box.place(&brace, (width - brace.width) / 2.0, y);
if let Some(r) = &r {
math_box.place(r, (width - r.width) / 2.0, y + brace.height + label_gap + r.depth);
}
} else {
let y = -(b.depth + gap + brace.height);
math_box.place(&brace, (width - brace.width) / 2.0, y);
if let Some(r) = &r {
math_box.place(r, (width - r.width) / 2.0, y - brace.depth - label_gap - r.height);
}
}
Ok(math_box)
}
fn compose_arrow(symbol: char, above: Option<&Node>, below: Option<&Node>, e: Style, ctx: &mut Context) -> R<MathBox> {
compose_wide_arrow(symbol, above, below, 0.0, e, ctx)
}
pub(crate) fn compose_wide_arrow(symbol: char, above: Option<&Node>, below: Option<&Node>, minimum: f32, e: Style, ctx: &mut Context) -> R<MathBox> {
let s = above.map(|n| compose(n, e.sup(), ctx)).transpose()?;
let d = below.map(|n| compose(n, e.sub(), ctx)).transpose()?;
let labels = s.as_ref().map_or(0.0, |s| s.width).max(d.as_ref().map_or(0.0, |d| d.width));
let arrow = stretch::horizontal(symbol, (labels + em(e, 0.8)).max(minimum), e)?;
let width = arrow.width.max(labels);
let mut math_box = MathBox::space(width);
math_box.place(&arrow, (width - arrow.width) / 2.0, 0.0);
if let Some(s) = &s {
let y = k(e, |c| c.stretch_stack_top_shift_up()).max(arrow.height + k(e, |c| c.stretch_stack_gap_above_min()) + s.depth);
math_box.place(s, (width - s.width) / 2.0, y);
}
if let Some(d) = &d {
let y = k(e, |c| c.stretch_stack_bottom_shift_down()).max(arrow.depth + k(e, |c| c.stretch_stack_gap_below_min()) + d.height);
math_box.place(d, (width - d.width) / 2.0, -y);
}
Ok(math_box)
}
fn compose_vertical_arrow(symbol: char, left: Option<&Node>, right: Option<&Node>, e: Style, ctx: &mut Context) -> R<MathBox> {
let arrow = stretch::delimiter(symbol, em(e, 2.0), e)?;
let es = e.sup();
let l = left.map(|n| compose(n, es, ctx)).transpose()?;
let r = right.map(|n| compose(n, es, ctx)).transpose()?;
let axis = k(e, |c| c.axis_height());
let slack = em(e, 0.17);
let mut math_box = MathBox::empty();
let center = |c: &MathBox| axis - (c.height - c.depth) / 2.0;
if let Some(l) = &l {
math_box.place(l, 0.0, center(l));
math_box.width = l.width + slack;
}
let x = math_box.width;
math_box.place(&arrow, x, 0.0);
math_box.width = x + arrow.width;
if let Some(r) = &r {
let x = math_box.width + slack;
math_box.place(r, x, center(r));
math_box.width = x + r.width;
}
Ok(math_box)
}
fn compose_canceled(base: &Node, kind: Cancel, e: Style, ctx: &mut Context) -> R<MathBox> {
let b = compose(base, e, ctx)?;
let t = k(e, |c| c.fraction_rule_thickness());
let slack = em(e, 0.05);
let (x0, x1, y0, y1) = (-slack, b.width + slack, -b.depth - slack, b.height + slack);
let mut math_box = b.clone();
math_box.single_glyph = false;
let mut rule = |a: (f32, f32), z: (f32, f32)| {
math_box.items.push(Item::Line { x1: a.0, y1: a.1, x2: z.0, y2: z.1, thickness: t, color: None });
};
if matches!(kind, Cancel::Bar | Cancel::X) {
rule((x0, y0), (x1, y1));
}
if matches!(kind, Cancel::Backslash | Cancel::X) {
rule((x0, y1), (x1, y0));
}
Ok(math_box)
}
fn compose_frame(base: &Node, shape: Frame, background: Option<&str>, border: Option<&str>, e: Style, ctx: &mut Context) -> R<MathBox> {
let b = compose(base, e, ctx)?;
let t = em(e, 0.04);
match shape {
Frame::Rectangle | Frame::Fill => {
let p = em(e, 0.3);
let width = b.width + 2.0 * (p + t);
let top = b.height + p + t;
let bottom = b.depth + p + t;
let mut math_box = MathBox::space(width);
if let Some(color) = background {
math_box.items.push(Item::Rule { x: 0.0, y: -bottom, width, height: top + bottom, color: Some(color.to_string()) });
}
math_box.place(&b, p + t, 0.0);
if shape == Frame::Rectangle {
let color = border.map(str::to_string);
math_box.items.push(Item::Rule { x: 0.0, y: top - t, width, height: t, color: color.clone() });
math_box.items.push(Item::Rule { x: 0.0, y: -bottom, width, height: t, color: color.clone() });
math_box.items.push(Item::Rule { x: 0.0, y: -bottom, width: t, height: top + bottom, color: color.clone() });
math_box.items.push(Item::Rule { x: width - t, y: -bottom, width: t, height: top + bottom, color });
}
math_box.height = top;
math_box.depth = bottom;
Ok(math_box)
}
Frame::Circle => {
let radius = (b.width.max(b.total()) / 2.0 + em(e, 0.12)).max(em(e, 0.42));
let width = 2.0 * radius + em(e, 0.06);
let cx = width / 2.0;
let cy = (b.height - b.depth) / 2.0;
let mut math_box = MathBox::space(width);
math_box.place(&b, cx - b.width / 2.0, 0.0);
math_box.items.push(Item::Ellipse { cx, cy, rx: radius, ry: radius, thickness: t * 1.5, color: None });
math_box.height = math_box.height.max(cy + radius + t);
math_box.depth = math_box.depth.max(radius - cy + t);
Ok(math_box)
}
}
}