mod accent;
mod cancel;
mod fenced;
mod fraction;
mod fragment;
mod line;
mod radical;
mod run;
mod scripts;
mod shaping;
mod table;
mod text;
use comemo::Tracked;
use typst_library::World;
use typst_library::diag::{At, SourceResult, warning};
use typst_library::engine::Engine;
use typst_library::foundations::{NativeElement, Packed, Resolve, Style, StyleChain};
use typst_library::introspection::{Counter, Locator};
use typst_library::layout::{
Abs, AlignElem, Axes, BlockElem, Em, FixedAlignment, Fragment, Frame, InlineItem,
OuterHAlignment, Point, Region, Regions, Size, SpecificAlignment, VAlignment,
};
use typst_library::math::ir::{
BoxItem, ExternalItem, MathComponent, MathItem, MathKind, MathProperties, MathmlItem,
resolve_equation,
};
use typst_library::math::{EquationElem, families};
use typst_library::model::ParElem;
use typst_library::routines::Arenas;
use typst_library::text::{
Font, FontFlags, FontInstance, TextEdgeBounds, TextElem, variant,
};
use typst_syntax::Span;
use typst_utils::{LazyHash, Numeric};
use self::accent::layout_accent;
use self::cancel::layout_cancel;
use self::fenced::layout_fenced;
use self::fraction::{layout_fraction, layout_skewed_fraction};
use self::fragment::{FrameFragment, MathFragment};
use self::line::layout_line;
use self::radical::layout_radical;
use self::run::{MathFragmentsExt, MathRun, MathRunFrameBuilder, layout_multiline};
use self::scripts::{layout_primes, layout_scripts};
use self::table::layout_table;
use self::text::{layout_glyph, layout_number, layout_text};
#[typst_macros::time(span = elem.span())]
pub fn layout_equation_inline(
elem: &Packed<EquationElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block.get(styles));
let span = elem.span();
let font = get_font(engine.world, styles, span)?;
warn_non_math_font(&font, engine, span);
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style);
let arenas = Arenas::default();
let item = resolve_equation(elem, engine, locator, &arenas, styles)?;
let mut ctx = MathContext::new(engine, region, font.clone());
let mut items = if !item.is_multiline() {
ctx.layout_into_fragments(&item, styles)?.into_par_items()
} else {
vec![InlineItem::Frame(ctx.layout_into_fragment(&item, styles)?.into_frame())]
};
if items.is_empty() {
items.push(InlineItem::Frame(Frame::soft(Size::zero())));
}
for item in &mut items {
let InlineItem::Frame(frame) = item else { continue };
let slack = styles.resolve(ParElem::leading) * 0.7;
let (t, b) = font.edges(
styles.get(TextElem::top_edge),
styles.get(TextElem::bottom_edge),
styles.resolve(TextElem::size),
TextEdgeBounds::Frame(frame),
);
let ascent = t.max(frame.ascent() - slack);
let descent = b.max(frame.descent() - slack);
frame.translate(Point::with_y(ascent - frame.baseline()));
frame.size_mut().y = ascent + descent;
}
Ok(items)
}
#[typst_macros::time(span = elem.span())]
pub fn layout_equation_block(
elem: &Packed<EquationElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
assert!(elem.block.get(styles));
let span = elem.span();
let font = get_font(engine.world, styles, span)?;
warn_non_math_font(&font, engine, span);
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style);
let arenas = Arenas::default();
let item = resolve_equation(elem, engine, locator.relayout(), &arenas, styles)?;
let mut ctx = MathContext::new(engine, regions.base(), font.clone());
let full_equation_builder = if let MathItem::Component(MathComponent {
kind: MathKind::Multiline(multi),
styles,
..
}) = item
{
layout_multiline(&multi, &mut ctx, styles)?
} else {
ctx.layout_into_fragments(&item, styles)?.into_frame().into()
};
let width = full_equation_builder.size.x;
let equation_builders = if styles.get(BlockElem::breakable) {
let mut rows = full_equation_builder.frames.into_iter().peekable();
let mut equation_builders = vec![];
let mut last_first_pos = Point::zero();
let mut regions = regions;
while let Some(&(_, first_pos)) = rows.peek() {
last_first_pos = first_pos;
let mut frames = vec![];
let mut height = Abs::zero();
while let Some((sub, pos)) = rows.peek() {
let mut pos = *pos;
pos.y -= first_pos.y;
if !regions.size.y.fits(sub.height() + pos.y)
&& (regions.may_progress()
|| (regions.may_break() && !frames.is_empty()))
{
break;
}
let (sub, _) = rows.next().unwrap();
height = height.max(pos.y + sub.height());
frames.push((sub, pos));
}
equation_builders
.push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
regions.next();
}
if let Some(equation_builder) = equation_builders.last_mut() {
equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
pos.y -= last_first_pos.y;
(frame, pos)
}));
let height = equation_builder
.frames
.iter()
.map(|(frame, pos)| frame.height() + pos.y)
.max()
.unwrap_or(equation_builder.size.y);
equation_builder.size.y = height;
}
if equation_builders.is_empty() {
equation_builders
.push(MathRunFrameBuilder { frames: vec![], size: Size::zero() });
}
equation_builders
} else {
vec![full_equation_builder]
};
let Some(numbering) = elem.numbering.get_ref(styles) else {
let frames = equation_builders
.into_iter()
.map(MathRunFrameBuilder::build_aligned)
.collect();
return Ok(Fragment::frames(frames));
};
let pod = Region::new(regions.base(), Axes::splat(false));
let counter = Counter::of(EquationElem::ELEM)
.display_at(engine, elem.location().unwrap(), styles, numbering, span)?
.spanned(span);
let mut locator = locator.split();
let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
let number_align = match elem.number_align.get(styles) {
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
};
let region_count = equation_builders.len();
let frames = equation_builders
.into_iter()
.map(|builder| {
if builder.frames.is_empty() && region_count > 1 {
return builder.build_aligned();
}
add_equation_number(
builder,
number.clone(),
number_align.resolve(styles),
styles.get(AlignElem::alignment).resolve(styles).x,
regions.size.x,
full_number_width,
)
})
.collect();
Ok(Fragment::frames(frames))
}
fn add_equation_number(
equation_builder: MathRunFrameBuilder,
number: Frame,
number_align: Axes<FixedAlignment>,
equation_align: FixedAlignment,
region_size_x: Abs,
full_number_width: Abs,
) -> Frame {
let first =
equation_builder.frames.first().map_or(
(equation_builder.size, Point::zero(), Abs::zero()),
|(frame, pos)| (frame.size(), *pos, frame.baseline()),
);
let last =
equation_builder.frames.last().map_or(
(equation_builder.size, Point::zero(), Abs::zero()),
|(frame, pos)| (frame.size(), *pos, frame.baseline()),
);
let line_count = equation_builder.frames.len();
let mut equation = equation_builder.build_aligned();
let width = if region_size_x.is_finite() {
region_size_x
} else {
equation.width() + 2.0 * full_number_width
};
let is_multiline = line_count >= 2;
let resizing_offset = resize_equation(
&mut equation,
&number,
number_align,
equation_align,
width,
is_multiline,
[first, last],
);
equation.translate(Point::with_x(match (equation_align, number_align.x) {
(FixedAlignment::Start, FixedAlignment::Start) => full_number_width,
(FixedAlignment::End, FixedAlignment::End) => -full_number_width,
_ => Abs::zero(),
}));
let x = match number_align.x {
FixedAlignment::Start => Abs::zero(),
FixedAlignment::End => equation.width() - number.width(),
_ => unreachable!(),
};
let y = {
let align_baselines = |(_, pos, baseline): (_, Point, Abs), number: &Frame| {
resizing_offset.y + pos.y + baseline - number.baseline()
};
match number_align.y {
FixedAlignment::Start => align_baselines(first, &number),
FixedAlignment::Center if !is_multiline => align_baselines(first, &number),
FixedAlignment::Center => (equation.height() - number.height()) / 2.0,
FixedAlignment::End => align_baselines(last, &number),
}
};
equation.push_frame(Point::new(x, y), number);
equation
}
fn resize_equation(
equation: &mut Frame,
number: &Frame,
number_align: Axes<FixedAlignment>,
equation_align: FixedAlignment,
width: Abs,
is_multiline: bool,
[first, last]: [(Axes<Abs>, Point, Abs); 2],
) -> Point {
if matches!(number_align.y, FixedAlignment::Center if is_multiline) {
return equation.resize(
Size::new(width, equation.height().max(number.height())),
Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Center),
);
}
let excess_above = Abs::zero().max({
if !is_multiline || matches!(number_align.y, FixedAlignment::Start) {
let (.., baseline) = first;
number.baseline() - baseline
} else {
Abs::zero()
}
});
let excess_below = Abs::zero().max({
if !is_multiline || matches!(number_align.y, FixedAlignment::End) {
let (size, .., baseline) = last;
(number.height() - number.baseline()) - (size.y - baseline)
} else {
Abs::zero()
}
});
let resizing_offset = equation.resize(
Size::new(width, equation.height() + excess_above + excess_below),
Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Start),
);
equation.translate(Point::with_y(excess_above));
resizing_offset + Point::with_y(excess_above)
}
struct MathContext<'v, 'e> {
engine: &'v mut Engine<'e>,
region: Region,
fonts_stack: Vec<FontInstance>,
fragments: MathRun,
}
impl<'v, 'e> MathContext<'v, 'e> {
fn new(engine: &'v mut Engine<'e>, base: Size, font: FontInstance) -> Self {
Self {
engine,
region: Region::new(base, Axes::splat(false)),
fonts_stack: vec![font],
fragments: vec![],
}
}
#[inline]
fn font(&self) -> &FontInstance {
self.fonts_stack.last().unwrap()
}
fn push(&mut self, fragment: impl Into<MathFragment>) {
self.fragments.push(fragment.into());
}
fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) {
self.fragments.extend(fragments);
}
fn layout_into_fragments(
&mut self,
item: &MathItem,
styles: StyleChain,
) -> SourceResult<MathRun> {
let start = self.fragments.len();
self.layout_into_self(item, styles)?;
Ok(self.fragments.drain(start..).collect())
}
fn layout_into_fragment(
&mut self,
item: &MathItem,
styles: StyleChain,
) -> SourceResult<MathFragment> {
let fragments = self.layout_into_fragments(item, styles)?;
if fragments.len() == 1 {
return Ok(fragments.into_iter().next().unwrap());
}
let text_like = fragments
.iter()
.filter(|e| e.math_size().is_some())
.all(|e| e.is_text_like());
let styles = item.styles().unwrap_or(styles);
let props = MathProperties::default(styles, Span::detached());
let frame = fragments.into_frame();
Ok(FrameFragment::new(&props, styles, frame)
.with_text_like(text_like)
.into())
}
fn layout_into_self(
&mut self,
item: &MathItem,
styles: StyleChain,
) -> SourceResult<()> {
let outer_styles = item.styles().unwrap_or(styles);
let outer_font = outer_styles.get_ref(TextElem::font);
for item in item.as_slice() {
let styles = item.styles().unwrap_or(outer_styles);
if styles != outer_styles && styles.get_ref(TextElem::font) != outer_font {
self.fonts_stack
.push(get_font(self.engine.world, styles, item.span())?);
let scale_style = style_for_script_scale(self.font());
layout_realized(item, self, styles.chain(&scale_style))?;
self.fonts_stack.pop();
} else {
layout_realized(item, self, styles)?;
}
}
Ok(())
}
}
fn layout_realized(
item: &MathItem,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let comp = match item {
MathItem::Component(comp) => comp,
MathItem::Spacing(amount, font_size, _) => {
ctx.push(MathFragment::Space(amount.at(*font_size)));
return Ok(());
}
MathItem::Space => {
ctx.push(MathFragment::Space(ctx.font().math().space_width.resolve(styles)));
return Ok(());
}
MathItem::Tag(tag) => {
ctx.push(MathFragment::Tag(tag.clone()));
return Ok(());
}
};
let props = &comp.props;
if let Some(lspace) = props.lspace
&& !props.align_form_infix
&& !lspace.is_zero()
{
let width = lspace.at(styles.resolve(TextElem::size));
ctx.push(MathFragment::Space(width));
}
match &comp.kind {
MathKind::Box(item) => layout_box(item, ctx, styles, props)?,
MathKind::Mathml(item) => layout_mathml(item, ctx, styles, props)?,
MathKind::External(item) => layout_external(item, ctx, styles, props)?,
MathKind::Glyph(item) => layout_glyph(item, ctx, styles, props)?,
MathKind::Cancel(item) => layout_cancel(item, ctx, styles, props)?,
MathKind::Radical(item) => layout_radical(item, ctx, styles, props)?,
MathKind::Line(item) => layout_line(item, ctx, styles, props)?,
MathKind::Accent(item) => layout_accent(item, ctx, styles, props)?,
MathKind::Scripts(item) => layout_scripts(item, ctx, styles, props)?,
MathKind::Primes(item) => layout_primes(item, ctx, styles, props)?,
MathKind::Table(item) => layout_table(item, ctx, styles, props)?,
MathKind::Fraction(item) => layout_fraction(item, ctx, styles, props)?,
MathKind::SkewedFraction(item) => {
layout_skewed_fraction(item, ctx, styles, props)?
}
MathKind::Text(item) => layout_text(item, ctx, styles, props)?,
MathKind::Number(item) => layout_number(item, ctx, styles, props)?,
MathKind::Fenced(item) => layout_fenced(item, ctx, styles, props)?,
MathKind::Multiline(item) => {
let mut frame = layout_multiline(item, ctx, styles)?.build_unaligned();
if item.centered {
let axis = ctx.font().math().axis_height.resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis);
}
ctx.push(FrameFragment::new(props, styles, frame));
}
MathKind::Group(_) => {
let fragment = ctx.layout_into_fragment(item, styles)?;
let italics = fragment.italics_correction();
let accent_attach = fragment.accent_attach();
ctx.push(
FrameFragment::new(props, styles, fragment.into_frame())
.with_italics_correction(italics)
.with_accent_attach(accent_attach),
);
}
}
if let Some(rspace) = props.rspace
&& !rspace.is_zero()
{
let width = rspace.at(styles.resolve(TextElem::size));
ctx.push(MathFragment::Space(width));
}
Ok(())
}
fn layout_mathml(
item: &MathmlItem,
ctx: &mut MathContext,
_styles: StyleChain,
_props: &MathProperties,
) -> SourceResult<()> {
ctx.engine.sink.warn(warning!(
item.elem.span(),
"MathML element was ignored during paged export",
));
Ok(())
}
fn layout_box(
item: &BoxItem,
ctx: &mut MathContext,
styles: StyleChain,
props: &MathProperties,
) -> SourceResult<()> {
let frame = crate::inline::layout_box(
item.elem,
ctx.engine,
item.locator.relayout(),
styles,
ctx.region.size,
)?;
ctx.push(FrameFragment::new(props, styles, frame));
Ok(())
}
fn layout_external(
item: &ExternalItem,
ctx: &mut MathContext,
styles: StyleChain,
props: &MathProperties,
) -> SourceResult<()> {
let mut frame = crate::layout_frame(
ctx.engine,
item.content,
item.locator.relayout(),
styles,
ctx.region,
)?;
if !frame.has_baseline() {
let axis = ctx.font().math().axis_height.resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis);
}
ctx.push(FrameFragment::new(props, styles, frame));
Ok(())
}
fn style_for_script_scale(font: &FontInstance) -> LazyHash<Style> {
EquationElem::script_scale
.set((
font.math().script_percent_scale_down,
font.math().script_script_percent_scale_down,
))
.wrap()
}
fn get_font(
world: Tracked<dyn World + '_>,
styles: StyleChain,
span: Span,
) -> SourceResult<FontInstance> {
let variant = variant(styles);
let size = styles.resolve(TextElem::size);
let variations = styles.get_cloned(TextElem::variations);
families(styles)
.find_map(|family| {
world
.book()
.select(family.as_str(), variant)
.and_then(|id| world.font(id))
.filter(|_| family.covers().is_none())
.map(|font| font.instantiate(variant, size, &variations))
})
.ok_or("no font could be found")
.at(span)
}
fn warn_non_math_font(font: &Font, engine: &mut Engine, span: Span) {
if !font.info().flags.contains(FontFlags::MATH) {
engine.sink.warn(warning!(
span,
"current font is not designed for math";
hint: "rendering may be poor";
))
}
}