mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
433 lines
13 KiB
Rust
433 lines
13 KiB
Rust
//! Mathematical formulas.
|
|
|
|
#[macro_use]
|
|
mod ctx;
|
|
mod accent;
|
|
mod align;
|
|
mod attach;
|
|
mod cancel;
|
|
mod delimited;
|
|
mod frac;
|
|
mod fragment;
|
|
mod matrix;
|
|
mod op;
|
|
mod root;
|
|
mod row;
|
|
mod spacing;
|
|
mod stretch;
|
|
mod style;
|
|
mod underover;
|
|
|
|
pub use self::accent::*;
|
|
pub use self::align::*;
|
|
pub use self::attach::*;
|
|
pub use self::cancel::*;
|
|
pub use self::delimited::*;
|
|
pub use self::frac::*;
|
|
pub use self::matrix::*;
|
|
pub use self::op::*;
|
|
pub use self::root::*;
|
|
pub use self::style::*;
|
|
pub use self::underover::*;
|
|
|
|
use ttf_parser::{GlyphId, Rect};
|
|
use typst::eval::{Module, Scope};
|
|
use typst::font::{Font, FontWeight};
|
|
use typst::model::Guard;
|
|
use unicode_math_class::MathClass;
|
|
|
|
use self::ctx::*;
|
|
use self::fragment::*;
|
|
use self::row::*;
|
|
use self::spacing::*;
|
|
use crate::layout::{HElem, ParElem, Spacing};
|
|
use crate::meta::Refable;
|
|
use crate::meta::{Count, Counter, CounterUpdate, LocalName, Numbering};
|
|
use crate::prelude::*;
|
|
use crate::text::{
|
|
families, variant, FontFamily, FontList, LinebreakElem, SpaceElem, TextElem, TextSize,
|
|
};
|
|
|
|
/// Create a module with all math definitions.
|
|
pub fn module() -> Module {
|
|
let mut math = Scope::deduplicating();
|
|
math.define("equation", EquationElem::func());
|
|
math.define("text", TextElem::func());
|
|
|
|
// Grouping.
|
|
math.define("lr", LrElem::func());
|
|
math.define("abs", abs);
|
|
math.define("norm", norm);
|
|
math.define("floor", floor);
|
|
math.define("ceil", ceil);
|
|
math.define("round", round);
|
|
|
|
// Attachments and accents.
|
|
math.define("attach", AttachElem::func());
|
|
math.define("scripts", ScriptsElem::func());
|
|
math.define("limits", LimitsElem::func());
|
|
math.define("accent", AccentElem::func());
|
|
math.define("underline", UnderlineElem::func());
|
|
math.define("overline", OverlineElem::func());
|
|
math.define("underbrace", UnderbraceElem::func());
|
|
math.define("overbrace", OverbraceElem::func());
|
|
math.define("underbracket", UnderbracketElem::func());
|
|
math.define("overbracket", OverbracketElem::func());
|
|
math.define("cancel", CancelElem::func());
|
|
|
|
// Fractions and matrix-likes.
|
|
math.define("frac", FracElem::func());
|
|
math.define("binom", BinomElem::func());
|
|
math.define("vec", VecElem::func());
|
|
math.define("mat", MatElem::func());
|
|
math.define("cases", CasesElem::func());
|
|
|
|
// Roots.
|
|
math.define("sqrt", sqrt);
|
|
math.define("root", RootElem::func());
|
|
|
|
// Styles.
|
|
math.define("upright", upright);
|
|
math.define("bold", bold);
|
|
math.define("italic", italic);
|
|
math.define("serif", serif);
|
|
math.define("sans", sans);
|
|
math.define("cal", cal);
|
|
math.define("frak", frak);
|
|
math.define("mono", mono);
|
|
math.define("bb", bb);
|
|
|
|
// Text operators.
|
|
math.define("op", OpElem::func());
|
|
op::define(&mut math);
|
|
|
|
// Spacings.
|
|
spacing::define(&mut math);
|
|
|
|
// Symbols.
|
|
for (name, symbol) in crate::symbols::SYM {
|
|
math.define(*name, symbol.clone());
|
|
}
|
|
|
|
Module::new("math").with_scope(math)
|
|
}
|
|
|
|
/// A mathematical equation.
|
|
///
|
|
/// Can be displayed inline with text or as a separate block.
|
|
///
|
|
/// ## Example
|
|
/// ```example
|
|
/// #set text(font: "New Computer Modern")
|
|
///
|
|
/// Let $a$, $b$, and $c$ be the side
|
|
/// lengths of right-angled triangle.
|
|
/// Then, we know that:
|
|
/// $ a^2 + b^2 = c^2 $
|
|
///
|
|
/// Prove by induction:
|
|
/// $ sum_(k=1)^n k = (n(n+1)) / 2 $
|
|
/// ```
|
|
///
|
|
/// ## Syntax
|
|
/// This function also has dedicated syntax: Write mathematical markup within
|
|
/// dollar signs to create an equation. Starting and ending the equation with at
|
|
/// least one space lifts it into a separate block that is centered
|
|
/// horizontally. For more details about math syntax, see the
|
|
/// [main math page]($category/math).
|
|
///
|
|
/// Display: Equation
|
|
/// Category: math
|
|
#[element(
|
|
Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable
|
|
)]
|
|
pub struct EquationElem {
|
|
/// Whether the equation is displayed as a separate block.
|
|
#[default(false)]
|
|
pub block: bool,
|
|
|
|
/// How to [number]($func/numbering) block-level equations.
|
|
///
|
|
/// ```example
|
|
/// #set math.equation(numbering: "(1)")
|
|
///
|
|
/// We define:
|
|
/// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
|
|
///
|
|
/// With @ratio, we get:
|
|
/// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
|
|
/// ```
|
|
pub numbering: Option<Numbering>,
|
|
|
|
/// The contents of the equation.
|
|
#[required]
|
|
pub body: Content,
|
|
}
|
|
|
|
impl Synthesize for EquationElem {
|
|
fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
|
|
self.push_block(self.block(styles));
|
|
self.push_numbering(self.numbering(styles));
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Show for EquationElem {
|
|
#[tracing::instrument(name = "EquationElem::show", skip_all)]
|
|
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
|
let mut realized = self.clone().pack().guarded(Guard::Base(Self::func()));
|
|
if self.block(styles) {
|
|
realized = realized.aligned(Axes::with_x(Some(Align::Center.into())))
|
|
}
|
|
Ok(realized)
|
|
}
|
|
}
|
|
|
|
impl Finalize for EquationElem {
|
|
fn finalize(&self, realized: Content, _: StyleChain) -> Content {
|
|
realized
|
|
.styled(TextElem::set_weight(FontWeight::from_number(450)))
|
|
.styled(TextElem::set_font(FontList(vec![FontFamily::new(
|
|
"New Computer Modern Math",
|
|
)])))
|
|
}
|
|
}
|
|
|
|
impl Layout for EquationElem {
|
|
#[tracing::instrument(name = "EquationElem::layout", skip_all)]
|
|
fn layout(
|
|
&self,
|
|
vt: &mut Vt,
|
|
styles: StyleChain,
|
|
regions: Regions,
|
|
) -> SourceResult<Fragment> {
|
|
const NUMBER_GUTTER: Em = Em::new(0.5);
|
|
|
|
let block = self.block(styles);
|
|
|
|
// Find a math font.
|
|
let variant = variant(styles);
|
|
let world = vt.world;
|
|
let Some(font) = families(styles)
|
|
.find_map(|family| {
|
|
let id = world.book().select(family.as_str(), variant)?;
|
|
let font = world.font(id)?;
|
|
let _ = font.ttf().tables().math?.constants?;
|
|
Some(font)
|
|
})
|
|
else {
|
|
bail!(self.span(), "current font does not support math");
|
|
};
|
|
|
|
let mut ctx = MathContext::new(vt, styles, regions, &font, block);
|
|
let mut frame = ctx.layout_frame(self)?;
|
|
|
|
if block {
|
|
if let Some(numbering) = self.numbering(styles) {
|
|
let pod = Regions::one(regions.base(), Axes::splat(false));
|
|
let counter = Counter::of(Self::func())
|
|
.display(Some(numbering), false)
|
|
.layout(vt, styles, pod)?
|
|
.into_frame();
|
|
|
|
let width = if regions.size.x.is_finite() {
|
|
regions.size.x
|
|
} else {
|
|
frame.width()
|
|
+ 2.0 * (counter.width() + NUMBER_GUTTER.resolve(styles))
|
|
};
|
|
|
|
let height = frame.height().max(counter.height());
|
|
frame.resize(Size::new(width, height), Align::CENTER_HORIZON);
|
|
|
|
let x = if TextElem::dir_in(styles).is_positive() {
|
|
frame.width() - counter.width()
|
|
} else {
|
|
Abs::zero()
|
|
};
|
|
let y = (frame.height() - counter.height()) / 2.0;
|
|
|
|
frame.push_frame(Point::new(x, y), counter)
|
|
}
|
|
} else {
|
|
let slack = ParElem::leading_in(styles) * 0.7;
|
|
let top_edge = TextElem::top_edge_in(styles).resolve(styles, font.metrics());
|
|
let bottom_edge =
|
|
-TextElem::bottom_edge_in(styles).resolve(styles, font.metrics());
|
|
|
|
let ascent = top_edge.max(frame.ascent() - slack);
|
|
let descent = bottom_edge.max(frame.descent() - slack);
|
|
frame.translate(Point::with_y(ascent - frame.baseline()));
|
|
frame.size_mut().y = ascent + descent;
|
|
}
|
|
|
|
// Apply metadata.
|
|
frame.meta(styles, false);
|
|
|
|
Ok(Fragment::frame(frame))
|
|
}
|
|
}
|
|
|
|
impl Count for EquationElem {
|
|
fn update(&self) -> Option<CounterUpdate> {
|
|
(self.block(StyleChain::default())
|
|
&& self.numbering(StyleChain::default()).is_some())
|
|
.then(|| CounterUpdate::Step(NonZeroUsize::ONE))
|
|
}
|
|
}
|
|
|
|
impl LocalName for EquationElem {
|
|
fn local_name(&self, lang: Lang) -> &'static str {
|
|
match lang {
|
|
Lang::ARABIC => "معادلة",
|
|
Lang::BOKMÅL => "Ligning",
|
|
Lang::CHINESE => "等式",
|
|
Lang::CZECH => "Rovnice",
|
|
Lang::FRENCH => "Équation",
|
|
Lang::GERMAN => "Gleichung",
|
|
Lang::ITALIAN => "Equazione",
|
|
Lang::NYNORSK => "Likning",
|
|
Lang::POLISH => "Równanie",
|
|
Lang::PORTUGUESE => "Equação",
|
|
Lang::RUSSIAN => "Уравнение",
|
|
Lang::SLOVENIAN => "Enačba",
|
|
Lang::SPANISH => "Ecuación",
|
|
Lang::UKRAINIAN => "Рівняння",
|
|
Lang::VIETNAMESE => "Phương trình",
|
|
Lang::ENGLISH | _ => "Equation",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Refable for EquationElem {
|
|
fn reference(
|
|
&self,
|
|
vt: &mut Vt,
|
|
supplement: Option<Content>,
|
|
lang: Lang,
|
|
) -> SourceResult<Content> {
|
|
// first we create the supplement of the heading
|
|
let mut supplement =
|
|
supplement.unwrap_or_else(|| TextElem::packed(self.local_name(lang)));
|
|
|
|
// we append a space if the supplement is not empty
|
|
if !supplement.is_empty() {
|
|
supplement += TextElem::packed('\u{a0}')
|
|
};
|
|
|
|
// we check for a numbering
|
|
let Some(numbering) = self.numbering(StyleChain::default()) else {
|
|
bail!(self.span(), "only numbered equations can be referenced");
|
|
};
|
|
|
|
// we get the counter and display it
|
|
let numbers = Counter::of(Self::func())
|
|
.at(vt, self.0.location().expect("missing location"))?
|
|
.display(vt, &numbering.trimmed())?;
|
|
|
|
Ok(supplement + numbers)
|
|
}
|
|
|
|
fn numbering(&self) -> Option<Numbering> {
|
|
self.numbering(StyleChain::default())
|
|
}
|
|
|
|
fn counter(&self) -> Counter {
|
|
Counter::of(Self::func())
|
|
}
|
|
}
|
|
|
|
pub trait LayoutMath {
|
|
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
|
|
}
|
|
|
|
impl LayoutMath for EquationElem {
|
|
#[tracing::instrument(skip(ctx))]
|
|
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
|
self.body().layout_math(ctx)
|
|
}
|
|
}
|
|
|
|
impl LayoutMath for Content {
|
|
#[tracing::instrument(skip(ctx))]
|
|
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
|
// Directly layout the body of nested equations instead of handling it
|
|
// like a normal equation so that things like this work:
|
|
// ```
|
|
// #let my = $pi$
|
|
// $ my r^2 $
|
|
// ```
|
|
if let Some(elem) = self.to::<EquationElem>() {
|
|
return elem.layout_math(ctx);
|
|
}
|
|
|
|
if let Some(realized) = ctx.realize(self)? {
|
|
return realized.layout_math(ctx);
|
|
}
|
|
|
|
if let Some(children) = self.to_sequence() {
|
|
for child in children {
|
|
child.layout_math(ctx)?;
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some((elem, styles)) = self.to_styled() {
|
|
if TextElem::font_in(ctx.styles().chain(styles))
|
|
!= TextElem::font_in(ctx.styles())
|
|
{
|
|
let frame = ctx.layout_content(self)?;
|
|
ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
|
|
return Ok(());
|
|
}
|
|
|
|
let prev_map = std::mem::replace(&mut ctx.local, styles.clone());
|
|
let prev_size = ctx.size;
|
|
ctx.local.apply(prev_map.clone());
|
|
ctx.size = TextElem::size_in(ctx.styles());
|
|
elem.layout_math(ctx)?;
|
|
ctx.size = prev_size;
|
|
ctx.local = prev_map;
|
|
return Ok(());
|
|
}
|
|
|
|
if self.is::<SpaceElem>() {
|
|
ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx)));
|
|
return Ok(());
|
|
}
|
|
|
|
if self.is::<LinebreakElem>() {
|
|
ctx.push(MathFragment::Linebreak);
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(elem) = self.to::<HElem>() {
|
|
if let Spacing::Rel(rel) = elem.amount() {
|
|
if rel.rel.is_zero() {
|
|
ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles())));
|
|
}
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(elem) = self.to::<TextElem>() {
|
|
let fragment = ctx.layout_text(elem)?;
|
|
ctx.push(fragment);
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(elem) = self.with::<dyn LayoutMath>() {
|
|
return elem.layout_math(ctx);
|
|
}
|
|
|
|
let mut frame = ctx.layout_content(self)?;
|
|
if !frame.has_baseline() {
|
|
let axis = scaled!(ctx, axis_height);
|
|
frame.set_baseline(frame.height() / 2.0 + axis);
|
|
}
|
|
ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
|
|
|
|
Ok(())
|
|
}
|
|
}
|