diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/math | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/math')
| -rw-r--r-- | crates/typst-library/src/math/accent.rs | 147 | ||||
| -rw-r--r-- | crates/typst-library/src/math/attach.rs | 156 | ||||
| -rw-r--r-- | crates/typst-library/src/math/cancel.rs | 116 | ||||
| -rw-r--r-- | crates/typst-library/src/math/equation.rs | 256 | ||||
| -rw-r--r-- | crates/typst-library/src/math/frac.rs | 56 | ||||
| -rw-r--r-- | crates/typst-library/src/math/lr.rs | 135 | ||||
| -rw-r--r-- | crates/typst-library/src/math/matrix.rs | 448 | ||||
| -rw-r--r-- | crates/typst-library/src/math/mod.rs | 257 | ||||
| -rw-r--r-- | crates/typst-library/src/math/op.rs | 103 | ||||
| -rw-r--r-- | crates/typst-library/src/math/root.rs | 35 | ||||
| -rw-r--r-- | crates/typst-library/src/math/style.rs | 254 | ||||
| -rw-r--r-- | crates/typst-library/src/math/underover.rs | 156 |
12 files changed, 2119 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs new file mode 100644 index 00000000..a1c70b4a --- /dev/null +++ b/crates/typst-library/src/math/accent.rs @@ -0,0 +1,147 @@ +use crate::diag::bail; +use crate::foundations::{cast, elem, func, Content, NativeElement, Smart, Value}; +use crate::layout::{Length, Rel}; +use crate::math::Mathy; +use crate::text::TextElem; + +/// Attaches an accent to a base. +/// +/// # Example +/// ```example +/// $grave(a) = accent(a, `)$ \ +/// $arrow(a) = accent(a, arrow)$ \ +/// $tilde(a) = accent(a, \u{0303})$ +/// ``` +#[elem(Mathy)] +pub struct AccentElem { + /// The base to which the accent is applied. + /// May consist of multiple letters. + /// + /// ```example + /// $arrow(A B C)$ + /// ``` + #[required] + pub base: Content, + + /// The accent to apply to the base. + /// + /// Supported accents include: + /// + /// | Accent | Name | Codepoint | + /// | ------------- | --------------- | --------- | + /// | Grave | `grave` | <code>`</code> | + /// | Acute | `acute` | `´` | + /// | Circumflex | `hat` | `^` | + /// | Tilde | `tilde` | `~` | + /// | Macron | `macron` | `¯` | + /// | Dash | `dash` | `‾` | + /// | Breve | `breve` | `˘` | + /// | Dot | `dot` | `.` | + /// | Double dot, Diaeresis | `dot.double`, `diaer` | `¨` | + /// | Triple dot | `dot.triple` | <code>⃛</code> | + /// | Quadruple dot | `dot.quad` | <code>⃜</code> | + /// | Circle | `circle` | `∘` | + /// | Double acute | `acute.double` | `˝` | + /// | Caron | `caron` | `ˇ` | + /// | Right arrow | `arrow`, `->` | `→` | + /// | Left arrow | `arrow.l`, `<-` | `←` | + /// | Left/Right arrow | `arrow.l.r` | `↔` | + /// | Right harpoon | `harpoon` | `⇀` | + /// | Left harpoon | `harpoon.lt` | `↼` | + #[required] + pub accent: Accent, + + /// The size of the accent, relative to the width of the base. + pub size: Smart<Rel<Length>>, +} + +/// An accent character. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Accent(pub char); + +impl Accent { + /// Normalize a character into an accent. + pub fn new(c: char) -> Self { + Self(Self::combine(c).unwrap_or(c)) + } +} + +/// This macro generates accent-related functions. +/// +/// ```ignore +/// accents! { +/// '\u{0300}' | '`' => grave, +/// // ^^^^^^^^^ ^^^ ^^^^^ +/// // | | | +/// // | | +-- The name of the function. +/// // | +--------- The alternative characters that represent the accent. +/// // +---------------------- The primary character that represents the accent. +/// } +/// ``` +/// +/// When combined with the `Accent::combine` function, accent characters can be normalized +/// to the primary character. +macro_rules! accents { + ($($primary:literal $(| $alt:literal)* => $name:ident),* $(,)?) => { + impl Accent { + /// Normalize an accent to a combining one. + pub fn combine(c: char) -> Option<char> { + Some(match c { + $($primary $(| $alt)* => $primary,)* + _ => return None, + }) + } + } + + $( + /// The accent function for callable symbol definitions. + #[func] + pub fn $name( + /// The base to which the accent is applied. + base: Content, + /// The size of the accent, relative to the width of the base. + #[named] + size: Option<Smart<Rel<Length>>>, + ) -> Content { + let mut accent = AccentElem::new(base, Accent::new($primary)); + if let Some(size) = size { + accent = accent.with_size(size); + } + accent.pack() + } + )+ + }; +} + +// Keep it synced with the documenting table above. +accents! { + '\u{0300}' | '`' => grave, + '\u{0301}' | '´' => acute, + '\u{0302}' | '^' | 'ˆ' => hat, + '\u{0303}' | '~' | '∼' | '˜' => tilde, + '\u{0304}' | '¯' => macron, + '\u{0305}' | '-' | '‾' | '−' => dash, + '\u{0306}' | '˘' => breve, + '\u{0307}' | '.' | '˙' | '⋅' => dot, + '\u{0308}' | '¨' => dot_double, + '\u{20db}' => dot_triple, + '\u{20dc}' => dot_quad, + '\u{030a}' | '∘' | '○' => circle, + '\u{030b}' | '˝' => acute_double, + '\u{030c}' | 'ˇ' => caron, + '\u{20d6}' | '←' => arrow_l, + '\u{20d7}' | '→' | '⟶' => arrow, + '\u{20e1}' | '↔' | '⟷' => arrow_l_r, + '\u{20d0}' | '↼' => harpoon_lt, + '\u{20d1}' | '⇀' => harpoon, +} + +cast! { + Accent, + self => self.0.into_value(), + v: char => Self::new(v), + v: Content => match v.to_packed::<TextElem>() { + Some(elem) => Value::Str(elem.text().clone().into()).cast()?, + None => bail!("expected text"), + }, +} diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs new file mode 100644 index 00000000..74eabd10 --- /dev/null +++ b/crates/typst-library/src/math/attach.rs @@ -0,0 +1,156 @@ +use crate::foundations::{elem, Content, Packed, Smart}; +use crate::layout::{Length, Rel}; +use crate::math::{EquationElem, Mathy}; + +/// A base with optional attachments. +/// +/// ```example +/// $ attach( +/// Pi, t: alpha, b: beta, +/// tl: 1, tr: 2+3, bl: 4+5, br: 6, +/// ) $ +/// ``` +#[elem(Mathy)] +pub struct AttachElem { + /// The base to which things are attached. + #[required] + pub base: Content, + + /// The top attachment, smartly positioned at top-right or above the base. + /// + /// You can wrap the base in `{limits()}` or `{scripts()}` to override the + /// smart positioning. + pub t: Option<Content>, + + /// The bottom attachment, smartly positioned at the bottom-right or below + /// the base. + /// + /// You can wrap the base in `{limits()}` or `{scripts()}` to override the + /// smart positioning. + pub b: Option<Content>, + + /// The top-left attachment (before the base). + pub tl: Option<Content>, + + /// The bottom-left attachment (before base). + pub bl: Option<Content>, + + /// The top-right attachment (after the base). + pub tr: Option<Content>, + + /// The bottom-right attachment (after the base). + pub br: Option<Content>, +} + +impl Packed<AttachElem> { + /// If an AttachElem's base is also an AttachElem, merge attachments into the + /// base AttachElem where possible. + pub fn merge_base(&self) -> Option<Self> { + // Extract from an EquationElem. + let mut base = self.base(); + if let Some(equation) = base.to_packed::<EquationElem>() { + base = equation.body(); + } + + // Move attachments from elem into base where possible. + if let Some(base) = base.to_packed::<AttachElem>() { + let mut elem = self.clone(); + let mut base = base.clone(); + + macro_rules! merge { + ($content:ident) => { + if base.$content.is_none() && elem.$content.is_some() { + base.$content = elem.$content.clone(); + elem.$content = None; + } + }; + } + + merge!(t); + merge!(b); + merge!(tl); + merge!(tr); + merge!(bl); + merge!(br); + + elem.base = base.pack(); + return Some(elem); + } + + None + } +} + +/// Grouped primes. +/// +/// ```example +/// $ a'''_b = a^'''_b $ +/// ``` +/// +/// # Syntax +/// This function has dedicated syntax: use apostrophes instead of primes. They +/// will automatically attach to the previous element, moving superscripts to +/// the next level. +#[elem(Mathy)] +pub struct PrimesElem { + /// The number of grouped primes. + #[required] + pub count: usize, +} + +/// Forces a base to display attachments as scripts. +/// +/// ```example +/// $ scripts(sum)_1^2 != sum_1^2 $ +/// ``` +#[elem(Mathy)] +pub struct ScriptsElem { + /// The base to attach the scripts to. + #[required] + pub body: Content, +} + +/// Forces a base to display attachments as limits. +/// +/// ```example +/// $ limits(A)_1^2 != A_1^2 $ +/// ``` +#[elem(Mathy)] +pub struct LimitsElem { + /// The base to attach the limits to. + #[required] + pub body: Content, + + /// Whether to also force limits in inline equations. + /// + /// When applying limits globally (e.g., through a show rule), it is + /// typically a good idea to disable this. + #[default(true)] + pub inline: bool, +} + +/// Stretches a glyph. +/// +/// This function can also be used to automatically stretch the base of an +/// attachment, so that it fits the top and bottom attachments. +/// +/// Note that only some glyphs can be stretched, and which ones can depend on +/// the math font being used. However, most math fonts are the same in this +/// regard. +/// +/// ```example +/// $ H stretch(=)^"define" U + p V $ +/// $ f : X stretch(->>, size: #150%)_"surjective" Y $ +/// $ x stretch(harpoons.ltrb, size: #3em) y +/// stretch(\[, size: #150%) z $ +/// ``` +#[elem(Mathy)] +pub struct StretchElem { + /// The glyph to stretch. + #[required] + pub body: Content, + + /// The size to stretch to, relative to the maximum size of the glyph and + /// its attachments. + pub size: Smart<Rel<Length>>, +} diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs new file mode 100644 index 00000000..a72505c0 --- /dev/null +++ b/crates/typst-library/src/math/cancel.rs @@ -0,0 +1,116 @@ +use crate::foundations::{cast, elem, Content, Func, Smart}; +use crate::layout::{Abs, Angle, Length, Ratio, Rel}; +use crate::math::Mathy; +use crate::visualize::Stroke; + +/// Displays a diagonal line over a part of an equation. +/// +/// This is commonly used to show the elimination of a term. +/// +/// # Example +/// ```example +/// >>> #set page(width: 140pt) +/// Here, we can simplify: +/// $ (a dot b dot cancel(x)) / +/// cancel(x) $ +/// ``` +#[elem(Mathy)] +pub struct CancelElem { + /// The content over which the line should be placed. + #[required] + pub body: Content, + + /// The length of the line, relative to the length of the diagonal spanning + /// the whole element being "cancelled". A value of `{100%}` would then have + /// the line span precisely the element's diagonal. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ a + cancel(x, length: #200%) + /// - cancel(x, length: #200%) $ + /// ``` + #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] + pub length: Rel<Length>, + + /// Whether the cancel line should be inverted (flipped along the y-axis). + /// For the default angle setting, inverted means the cancel line + /// points to the top left instead of top right. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ (a cancel((b + c), inverted: #true)) / + /// cancel(b + c, inverted: #true) $ + /// ``` + #[default(false)] + pub inverted: bool, + + /// Whether two opposing cancel lines should be drawn, forming a cross over + /// the element. Overrides `inverted`. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi, cross: #true) $ + /// ``` + #[default(false)] + pub cross: bool, + + /// How much to rotate the cancel line. + /// + /// - If given an angle, the line is rotated by that angle clockwise with + /// respect to the y-axis. + /// - If `{auto}`, the line assumes the default angle; that is, along the + /// rising diagonal of the content box. + /// - If given a function `angle => angle`, the line is rotated, with + /// respect to the y-axis, by the angle returned by that function. The + /// function receives the default angle as its input. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi) + /// cancel(Pi, angle: #0deg) + /// cancel(Pi, angle: #45deg) + /// cancel(Pi, angle: #90deg) + /// cancel(1/(1+x), angle: #(a => a + 45deg)) + /// cancel(1/(1+x), angle: #(a => a + 90deg)) $ + /// ``` + pub angle: Smart<CancelAngle>, + + /// How to [stroke]($stroke) the cancel line. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel( + /// sum x, + /// stroke: #( + /// paint: red, + /// thickness: 1.5pt, + /// dash: "dashed", + /// ), + /// ) $ + /// ``` + #[resolve] + #[fold] + #[default(Stroke { + // Default stroke has 0.5pt for better visuals. + thickness: Smart::Custom(Abs::pt(0.5).into()), + ..Default::default() + })] + pub stroke: Stroke, +} + +/// Defines the cancel line. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CancelAngle { + Angle(Angle), + Func(Func), +} + +cast! { + CancelAngle, + self => match self { + Self::Angle(v) => v.into_value(), + Self::Func(v) => v.into_value() + }, + v: Angle => CancelAngle::Angle(v), + v: Func => CancelAngle::Func(v), +} diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs new file mode 100644 index 00000000..4edafe5e --- /dev/null +++ b/crates/typst-library/src/math/equation.rs @@ -0,0 +1,256 @@ +use std::num::NonZeroUsize; + +use typst_utils::NonZeroExt; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, + Synthesize, +}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::layout::{ + AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment, + VAlignment, +}; +use crate::math::{MathSize, MathVariant}; +use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; +use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; + +/// 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 $ +/// ``` +/// +/// By default, block-level equations will not break across pages. This can be +/// changed through `{show math.equation: set block(breakable: true)}`. +/// +/// # 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). +#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] +pub struct EquationElem { + /// Whether the equation is displayed as a separate block. + #[default(false)] + pub block: bool, + + /// How to [number]($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) $ + /// ``` + #[borrowed] + pub numbering: Option<Numbering>, + + /// The alignment of the equation numbering. + /// + /// By default, the alignment is `{end + horizon}`. For the horizontal + /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}` + /// of the text direction; for the vertical component, you can use + /// `{top}`, `{horizon}`, or `{bottom}`. + /// + /// ```example + /// #set math.equation(numbering: "(1)", number-align: bottom) + /// + /// We can calculate: + /// $ E &= sqrt(m_0^2 + p^2) \ + /// &approx 125 "GeV" $ + /// ``` + #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))] + pub number_align: SpecificAlignment<OuterHAlignment, VAlignment>, + + /// A supplement for the equation. + /// + /// For references to equations, this is added before the referenced number. + /// + /// If a function is specified, it is passed the referenced equation and + /// should return content. + /// + /// ```example + /// #set math.equation(numbering: "(1)", supplement: [Eq.]) + /// + /// We define: + /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> + /// + /// With @ratio, we get: + /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ + /// ``` + pub supplement: Smart<Option<Supplement>>, + + /// The contents of the equation. + #[required] + pub body: Content, + + /// The size of the glyphs. + #[internal] + #[default(MathSize::Text)] + #[ghost] + pub size: MathSize, + + /// The style variant to select. + #[internal] + #[ghost] + pub variant: MathVariant, + + /// Affects the height of exponents. + #[internal] + #[default(false)] + #[ghost] + pub cramped: bool, + + /// Whether to use bold glyphs. + #[internal] + #[default(false)] + #[ghost] + pub bold: bool, + + /// Whether to use italic glyphs. + #[internal] + #[ghost] + pub italic: Smart<bool>, + + /// A forced class to use for all fragment. + #[internal] + #[ghost] + pub class: Option<MathClass>, +} + +impl Synthesize for Packed<EquationElem> { + fn synthesize( + &mut self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<()> { + let supplement = match self.as_ref().supplement(styles) { + Smart::Auto => TextElem::packed(Self::local_name_in(styles)), + Smart::Custom(None) => Content::empty(), + Smart::Custom(Some(supplement)) => { + supplement.resolve(engine, styles, [self.clone().pack()])? + } + }; + + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + Ok(()) + } +} + +impl Show for Packed<EquationElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + if self.block(styles) { + Ok(BlockElem::multi_layouter( + self.clone(), + engine.routines.layout_equation_block, + ) + .pack() + .spanned(self.span())) + } else { + Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline) + .pack() + .spanned(self.span())) + } + } +} + +impl ShowSet for Packed<EquationElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let mut out = Styles::new(); + if self.block(styles) { + out.set(AlignElem::set_alignment(Alignment::CENTER)); + out.set(BlockElem::set_breakable(false)); + out.set(ParLine::set_numbering(None)); + out.set(EquationElem::set_size(MathSize::Display)); + } else { + out.set(EquationElem::set_size(MathSize::Text)); + } + out.set(TextElem::set_weight(FontWeight::from_number(450))); + out.set(TextElem::set_font(FontList(vec![FontFamily::new( + "New Computer Modern Math", + )]))); + out + } +} + +impl Count for Packed<EquationElem> { + fn update(&self) -> Option<CounterUpdate> { + (self.block(StyleChain::default()) && self.numbering().is_some()) + .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +impl LocalName for Packed<EquationElem> { + const KEY: &'static str = "equation"; +} + +impl Refable for Packed<EquationElem> { + fn supplement(&self) -> Content { + // After synthesis, this should always be custom content. + match (**self).supplement(StyleChain::default()) { + Smart::Custom(Some(Supplement::Content(content))) => content, + _ => Content::empty(), + } + } + + fn counter(&self) -> Counter { + Counter::of(EquationElem::elem()) + } + + fn numbering(&self) -> Option<&Numbering> { + (**self).numbering(StyleChain::default()).as_ref() + } +} + +impl Outlinable for Packed<EquationElem> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { + if !self.block(StyleChain::default()) { + return Ok(None); + } + let Some(numbering) = self.numbering() else { + return Ok(None); + }; + + // After synthesis, this should always be custom content. + let mut supplement = match (**self).supplement(StyleChain::default()) { + Smart::Custom(Some(Supplement::Content(content))) => content, + _ => Content::empty(), + }; + + if !supplement.is_empty() { + supplement += TextElem::packed("\u{a0}"); + } + + let numbers = self.counter().display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; + + Ok(Some(supplement + numbers)) + } +} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs new file mode 100644 index 00000000..f5c4514d --- /dev/null +++ b/crates/typst-library/src/math/frac.rs @@ -0,0 +1,56 @@ +use typst_syntax::Spanned; + +use crate::diag::bail; +use crate::foundations::{elem, Content, Value}; +use crate::math::Mathy; + +/// A mathematical fraction. +/// +/// # Example +/// ```example +/// $ 1/2 < (x+1)/2 $ +/// $ ((x+1)) / 2 = frac(a, b) $ +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: Use a slash to turn neighbouring +/// expressions into a fraction. Multiple atoms can be grouped into a single +/// expression using round grouping parenthesis. Such parentheses are removed +/// from the output, but you can nest multiple to force them. +#[elem(title = "Fraction", Mathy)] +pub struct FracElem { + /// The fraction's numerator. + #[required] + pub num: Content, + + /// The fraction's denominator. + #[required] + pub denom: Content, +} + +/// A binomial expression. +/// +/// # Example +/// ```example +/// $ binom(n, k) $ +/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ +/// ``` +#[elem(title = "Binomial", Mathy)] +pub struct BinomElem { + /// The binomial's upper index. + #[required] + pub upper: Content, + + /// The binomial's lower index. + #[required] + #[variadic] + #[parse( + let values = args.all::<Spanned<Value>>()?; + if values.is_empty() { + // Prevents one element binomials + bail!(args.span, "missing argument: lower"); + } + values.into_iter().map(|spanned| spanned.v.display()).collect() + )] + pub lower: Vec<Content>, +} diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs new file mode 100644 index 00000000..07ab0dd4 --- /dev/null +++ b/crates/typst-library/src/math/lr.rs @@ -0,0 +1,135 @@ +use crate::foundations::{elem, func, Content, NativeElement, Smart}; +use crate::layout::{Length, Rel}; +use crate::math::Mathy; +use crate::text::TextElem; + +/// Scales delimiters. +/// +/// While matched delimiters scale by default, this can be used to scale +/// unmatched delimiters and to control the delimiter scaling more precisely. +#[elem(title = "Left/Right", Mathy)] +pub struct LrElem { + /// The size of the brackets, relative to the height of the wrapped content. + pub size: Smart<Rel<Length>>, + + /// The delimited content, including the delimiters. + #[required] + #[parse( + let mut arguments = args.all::<Content>()?.into_iter(); + let mut body = arguments.next().unwrap_or_default(); + arguments.for_each(|arg| body += TextElem::packed(',') + arg); + body + )] + pub body: Content, +} + +/// Scales delimiters vertically to the nearest surrounding `{lr()}` group. +/// +/// ```example +/// $ { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } $ +/// ``` +#[elem(Mathy)] +pub struct MidElem { + /// The content to be scaled. + #[required] + pub body: Content, +} + +/// Floors an expression. +/// +/// ```example +/// $ floor(x/2) $ +/// ``` +#[func] +pub fn floor( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to floor. + body: Content, +) -> Content { + delimited(body, '⌊', '⌋', size) +} + +/// Ceils an expression. +/// +/// ```example +/// $ ceil(x/2) $ +/// ``` +#[func] +pub fn ceil( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to ceil. + body: Content, +) -> Content { + delimited(body, '⌈', '⌉', size) +} + +/// Rounds an expression. +/// +/// ```example +/// $ round(x/2) $ +/// ``` +#[func] +pub fn round( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to round. + body: Content, +) -> Content { + delimited(body, '⌊', '⌉', size) +} + +/// Takes the absolute value of an expression. +/// +/// ```example +/// $ abs(x/2) $ +/// ``` +#[func] +pub fn abs( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to take the absolute value of. + body: Content, +) -> Content { + delimited(body, '|', '|', size) +} + +/// Takes the norm of an expression. +/// +/// ```example +/// $ norm(x/2) $ +/// ``` +#[func] +pub fn norm( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to take the norm of. + body: Content, +) -> Content { + delimited(body, '‖', '‖', size) +} + +fn delimited( + body: Content, + left: char, + right: char, + size: Option<Smart<Rel<Length>>>, +) -> Content { + let span = body.span(); + let mut elem = LrElem::new(Content::sequence([ + TextElem::packed(left), + body, + TextElem::packed(right), + ])); + // Push size only if size is provided + if let Some(size) = size { + elem.push_size(size); + } + elem.pack().spanned(span) +} diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs new file mode 100644 index 00000000..1c788995 --- /dev/null +++ b/crates/typst-library/src/math/matrix.rs @@ -0,0 +1,448 @@ +use smallvec::{smallvec, SmallVec}; +use typst_syntax::Spanned; +use typst_utils::Numeric; +use unicode_math_class::MathClass; + +use crate::diag::{bail, At, HintedStrResult, StrResult}; +use crate::foundations::{ + array, cast, dict, elem, Array, Content, Dict, Fold, NoneValue, Resolve, Smart, + StyleChain, Symbol, Value, +}; +use crate::layout::{Abs, Em, HAlignment, Length, Rel}; +use crate::math::Mathy; +use crate::visualize::Stroke; + +const DEFAULT_ROW_GAP: Em = Em::new(0.2); +const DEFAULT_COL_GAP: Em = Em::new(0.5); + +/// A column vector. +/// +/// Content in the vector's elements can be aligned with the +/// [`align`]($math.vec.align) parameter, or the `&` symbol. +/// +/// # Example +/// ```example +/// $ vec(a, b, c) dot vec(1, 2, 3) +/// = a + 2b + 3c $ +/// ``` +#[elem(title = "Vector", Mathy)] +pub struct VecElem { + /// The delimiter to use. + /// + /// Can be a single character specifying the left delimiter, in which case + /// the right delimiter is inferred. Otherwise, can be an array containing a + /// left and a right delimiter. + /// + /// ```example + /// #set math.vec(delim: "[") + /// $ vec(1, 2) $ + /// ``` + #[default(DelimiterPair::PAREN)] + pub delim: DelimiterPair, + + /// The horizontal alignment that each element should have. + /// + /// ```example + /// #set math.vec(align: right) + /// $ vec(-1, 1, -1) $ + /// ``` + #[resolve] + #[default(HAlignment::Center)] + pub align: HAlignment, + + /// The gap between elements. + /// + /// ```example + /// #set math.vec(gap: 1em) + /// $ vec(1, 2) $ + /// ``` + #[default(DEFAULT_ROW_GAP.into())] + pub gap: Rel<Length>, + + /// The elements of the vector. + #[variadic] + pub children: Vec<Content>, +} + +/// A matrix. +/// +/// The elements of a row should be separated by commas, while the rows +/// themselves should be separated by semicolons. The semicolon syntax merges +/// preceding arguments separated by commas into an array. You can also use this +/// special syntax of math function calls to define custom functions that take +/// 2D data. +/// +/// Content in cells can be aligned with the [`align`]($math.mat.align) +/// parameter, or content in cells that are in the same row can be aligned with +/// the `&` symbol. +/// +/// # Example +/// ```example +/// $ mat( +/// 1, 2, ..., 10; +/// 2, 2, ..., 10; +/// dots.v, dots.v, dots.down, dots.v; +/// 10, 10, ..., 10; +/// ) $ +/// ``` +#[elem(title = "Matrix", Mathy)] +pub struct MatElem { + /// The delimiter to use. + /// + /// Can be a single character specifying the left delimiter, in which case + /// the right delimiter is inferred. Otherwise, can be an array containing a + /// left and a right delimiter. + /// + /// ```example + /// #set math.mat(delim: "[") + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[default(DelimiterPair::PAREN)] + pub delim: DelimiterPair, + + /// The horizontal alignment that each cell should have. + /// + /// ```example + /// #set math.mat(align: right) + /// $ mat(-1, 1, 1; 1, -1, 1; 1, 1, -1) $ + /// ``` + #[resolve] + #[default(HAlignment::Center)] + pub align: HAlignment, + + /// Draws augmentation lines in a matrix. + /// + /// - `{none}`: No lines are drawn. + /// - A single number: A vertical augmentation line is drawn + /// after the specified column number. Negative numbers start from the end. + /// - A dictionary: With a dictionary, multiple augmentation lines can be + /// drawn both horizontally and vertically. Additionally, the style of the + /// lines can be set. The dictionary can contain the following keys: + /// - `hline`: The offsets at which horizontal lines should be drawn. + /// For example, an offset of `2` would result in a horizontal line + /// being drawn after the second row of the matrix. Accepts either an + /// integer for a single line, or an array of integers + /// for multiple lines. Like for a single number, negative numbers start from the end. + /// - `vline`: The offsets at which vertical lines should be drawn. + /// For example, an offset of `2` would result in a vertical line being + /// drawn after the second column of the matrix. Accepts either an + /// integer for a single line, or an array of integers + /// for multiple lines. Like for a single number, negative numbers start from the end. + /// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`, + /// takes on a thickness of 0.05em and square line caps. + /// + /// ```example + /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $ + /// // Equivalent to: + /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $ + /// ``` + /// + /// ```example + /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $ + /// ``` + #[resolve] + #[fold] + pub augment: Option<Augment>, + + /// The gap between rows and columns. + /// + /// This is a shorthand to set `row-gap` and `column-gap` to the same value. + /// + /// ```example + /// #set math.mat(gap: 1em) + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[external] + pub gap: Rel<Length>, + + /// The gap between rows. + /// + /// ```example + /// #set math.mat(row-gap: 1em) + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[parse( + let gap = args.named("gap")?; + args.named("row-gap")?.or(gap) + )] + #[default(DEFAULT_ROW_GAP.into())] + pub row_gap: Rel<Length>, + + /// The gap between columns. + /// + /// ```example + /// #set math.mat(column-gap: 1em) + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[parse(args.named("column-gap")?.or(gap))] + #[default(DEFAULT_COL_GAP.into())] + pub column_gap: Rel<Length>, + + /// An array of arrays with the rows of the matrix. + /// + /// ```example + /// #let data = ((1, 2, 3), (4, 5, 6)) + /// #let matrix = math.mat(..data) + /// $ v := matrix $ + /// ``` + #[variadic] + #[parse( + let mut rows = vec![]; + let mut width = 0; + + let values = args.all::<Spanned<Value>>()?; + if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) { + for Spanned { v, span } in values { + let array = v.cast::<Array>().at(span)?; + let row: Vec<_> = array.into_iter().map(Value::display).collect(); + width = width.max(row.len()); + rows.push(row); + } + } else { + rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()]; + } + + for row in &mut rows { + if row.len() < width { + row.resize(width, Content::empty()); + } + } + + rows + )] + pub rows: Vec<Vec<Content>>, +} + +/// A case distinction. +/// +/// Content across different branches can be aligned with the `&` symbol. +/// +/// # Example +/// ```example +/// $ f(x, y) := cases( +/// 1 "if" (x dot y)/2 <= 0, +/// 2 "if" x "is even", +/// 3 "if" x in NN, +/// 4 "else", +/// ) $ +/// ``` +#[elem(Mathy)] +pub struct CasesElem { + /// The delimiter to use. + /// + /// Can be a single character specifying the left delimiter, in which case + /// the right delimiter is inferred. Otherwise, can be an array containing a + /// left and a right delimiter. + /// + /// ```example + /// #set math.cases(delim: "[") + /// $ x = cases(1, 2) $ + /// ``` + #[default(DelimiterPair::BRACE)] + pub delim: DelimiterPair, + + /// Whether the direction of cases should be reversed. + /// + /// ```example + /// #set math.cases(reverse: true) + /// $ cases(1, 2) = x $ + /// ``` + #[default(false)] + pub reverse: bool, + + /// The gap between branches. + /// + /// ```example + /// #set math.cases(gap: 1em) + /// $ x = cases(1, 2) $ + /// ``` + #[default(DEFAULT_ROW_GAP.into())] + pub gap: Rel<Length>, + + /// The branches of the case distinction. + #[variadic] + pub children: Vec<Content>, +} + +/// A delimiter is a single character that is used to delimit a matrix, vector +/// or cases. The character has to be a Unicode codepoint tagged as a math +/// "opening", "closing" or "fence". +/// +/// Typically, the delimiter is stretched to fit the height of whatever it +/// delimits. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Delimiter(Option<char>); + +cast! { + Delimiter, + self => self.0.into_value(), + _: NoneValue => Self::none(), + v: Symbol => Self::char(v.get())?, + v: char => Self::char(v)?, +} + +impl Delimiter { + pub fn none() -> Self { + Self(None) + } + + pub fn char(c: char) -> StrResult<Self> { + if !matches!( + unicode_math_class::class(c), + Some(MathClass::Opening | MathClass::Closing | MathClass::Fence), + ) { + bail!("invalid delimiter: \"{}\"", c) + } + Ok(Self(Some(c))) + } + + pub fn get(self) -> Option<char> { + self.0 + } + + pub fn find_matching(self) -> Self { + match self.0 { + None => Self::none(), + Some('[') => Self(Some(']')), + Some(']') => Self(Some('[')), + Some('{') => Self(Some('}')), + Some('}') => Self(Some('{')), + Some(c) => match unicode_math_class::class(c) { + Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)), + Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)), + _ => Self(Some(c)), + }, + } + } +} + +/// A pair of delimiters (one closing, one opening) used for matrices, vectors +/// and cases. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct DelimiterPair { + open: Delimiter, + close: Delimiter, +} + +cast! { + DelimiterPair, + + self => array![self.open, self.close].into_value(), + + v: Array => match v.as_slice() { + [open, close] => Self { + open: open.clone().cast()?, + close: close.clone().cast()?, + }, + _ => bail!("expected 2 delimiters, found {}", v.len()) + }, + v: Delimiter => Self { open: v, close: v.find_matching() } +} + +impl DelimiterPair { + const PAREN: Self = Self { + open: Delimiter(Some('(')), + close: Delimiter(Some(')')), + }; + const BRACE: Self = Self { + open: Delimiter(Some('{')), + close: Delimiter(Some('}')), + }; + + /// The delimiter's opening character. + pub fn open(self) -> Option<char> { + self.open.get() + } + + /// The delimiter's closing character. + pub fn close(self) -> Option<char> { + self.close.get() + } +} + +/// Parameters specifying how augmentation lines +/// should be drawn on a matrix. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct Augment<T: Numeric = Length> { + pub hline: AugmentOffsets, + pub vline: AugmentOffsets, + pub stroke: Smart<Stroke<T>>, +} + +impl<T: Numeric + Fold> Fold for Augment<T> { + fn fold(self, outer: Self) -> Self { + Self { + stroke: match (self.stroke, outer.stroke) { + (Smart::Custom(inner), Smart::Custom(outer)) => { + Smart::Custom(inner.fold(outer)) + } + // Usually, folding an inner `auto` with an `outer` prefers + // the explicit `auto`. However, here `auto` means unspecified + // and thus we want `outer`. + (inner, outer) => inner.or(outer), + }, + ..self + } + } +} + +impl Resolve for Augment { + type Output = Augment<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + Augment { + hline: self.hline, + vline: self.vline, + stroke: self.stroke.resolve(styles), + } + } +} + +cast! { + Augment, + self => { + // if the stroke is auto and there is only one vertical line, + if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 { + return self.vline.0[0].into_value(); + } + + dict! { + "hline" => self.hline, + "vline" => self.vline, + "stroke" => self.stroke, + }.into_value() + }, + v: isize => Augment { + hline: AugmentOffsets::default(), + vline: AugmentOffsets(smallvec![v]), + stroke: Smart::Auto, + }, + mut dict: Dict => { + let mut take = |key| dict.take(key).ok().map(AugmentOffsets::from_value).transpose(); + let hline = take("hline")?.unwrap_or_default(); + let vline = take("vline")?.unwrap_or_default(); + let stroke = dict.take("stroke") + .ok() + .map(Stroke::from_value) + .transpose()? + .map(Smart::Custom) + .unwrap_or(Smart::Auto); + Augment { hline, vline, stroke } + }, +} + +cast! { + Augment<Abs>, + self => self.into_value(), +} + +/// The offsets at which augmentation lines should be drawn on a matrix. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct AugmentOffsets(pub SmallVec<[isize; 1]>); + +cast! { + AugmentOffsets, + self => self.0.into_value(), + v: isize => Self(smallvec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs new file mode 100644 index 00000000..53b1f072 --- /dev/null +++ b/crates/typst-library/src/math/mod.rs @@ -0,0 +1,257 @@ +//! Mathematical formulas. + +pub mod accent; +mod attach; +mod cancel; +mod equation; +mod frac; +mod lr; +mod matrix; +mod op; +mod root; +mod style; +mod underover; + +pub use self::accent::{Accent, AccentElem}; +pub use self::attach::*; +pub use self::cancel::*; +pub use self::equation::*; +pub use self::frac::*; +pub use self::lr::*; +pub use self::matrix::*; +pub use self::op::*; +pub use self::root::*; +pub use self::style::*; +pub use self::underover::*; + +use typst_utils::singleton; +use unicode_math_class::MathClass; + +use crate::foundations::{ + category, elem, Category, Content, Module, NativeElement, Scope, +}; +use crate::layout::{Em, HElem}; +use crate::text::TextElem; + +/// Typst has special [syntax]($syntax/#math) and library functions to typeset +/// mathematical formulas. Math formulas can be displayed inline with text or as +/// separate blocks. They will be typeset into their own block if they start and +/// end with at least one space (e.g. `[$ x^2 $]`). +/// +/// # Variables +/// In math, single letters are always displayed as is. Multiple letters, +/// however, are interpreted as variables and functions. To display multiple +/// letters verbatim, you can place them into quotes and to access single letter +/// variables, you can use the [hash syntax]($scripting/#expressions). +/// +/// ```example +/// $ A = pi r^2 $ +/// $ "area" = pi dot "radius"^2 $ +/// $ cal(A) := +/// { x in RR | x "is natural" } $ +/// #let x = 5 +/// $ #x < 17 $ +/// ``` +/// +/// # Symbols +/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like +/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in +/// different variants. You can select between different variants by applying +/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of +/// shorthand sequences like `=>` that approximate a symbol. When such a +/// shorthand exists, the symbol's documentation lists it. +/// +/// ```example +/// $ x < y => x gt.eq.not y $ +/// ``` +/// +/// # Line Breaks +/// Formulas can also contain line breaks. Each line can contain one or multiple +/// _alignment points_ (`&`) which are then aligned. +/// +/// ```example +/// $ sum_(k=0)^n k +/// &= 1 + ... + n \ +/// &= (n(n+1)) / 2 $ +/// ``` +/// +/// # Function calls +/// Math mode supports special function calls without the hash prefix. In these +/// "math calls", the argument list works a little differently than in code: +/// +/// - Within them, Typst is still in "math mode". Thus, you can write math +/// directly into them, but need to use hash syntax to pass code expressions +/// (except for strings, which are available in the math syntax). +/// - They support positional and named arguments, but don't support trailing +/// content blocks and argument spreading. +/// - They provide additional syntax for 2-dimensional argument lists. The +/// semicolon (`;`) merges preceding arguments separated by commas into an +/// array argument. +/// +/// ```example +/// $ frac(a^2, 2) $ +/// $ vec(1, 2, delim: "[") $ +/// $ mat(1, 2; 3, 4) $ +/// $ lim_x = +/// op("lim", limits: #true)_x $ +/// ``` +/// +/// To write a verbatim comma or semicolon in a math call, escape it with a +/// backslash. The colon on the other hand is only recognized in a special way +/// if directly preceded by an identifier, so to display it verbatim in those +/// cases, you can just insert a space before it. +/// +/// Functions calls preceded by a hash are normal code function calls and not +/// affected by these rules. +/// +/// # Alignment +/// When equations include multiple _alignment points_ (`&`), this creates +/// blocks of alternatingly right- and left-aligned columns. In the example +/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is +/// left-aligned. The word "given" is also left-aligned because `&&` creates two +/// alignment points in a row, alternating the alignment twice. `& &` and `&&` +/// behave exactly the same way. Meanwhile, "multiply by 7" is right-aligned +/// because just one `&` precedes it. Each alignment point simply alternates +/// between right-aligned/left-aligned. +/// +/// ```example +/// $ (3x + y) / 7 &= 9 && "given" \ +/// 3x + y &= 63 & "multiply by 7" \ +/// 3x &= 63 - y && "subtract y" \ +/// x &= 21 - y/3 & "divide by 3" $ +/// ``` +/// +/// # Math fonts +/// You can set the math font by with a [show-set rule]($styling/#show-rules) as +/// demonstrated below. Note that only special OpenType math fonts are suitable +/// for typesetting maths. +/// +/// ```example +/// #show math.equation: set text(font: "Fira Math") +/// $ sum_(i in NN) 1 + i $ +/// ``` +/// +/// # Math module +/// All math functions are part of the `math` [module]($scripting/#modules), +/// which is available by default in equations. Outside of equations, they can +/// be accessed with the `math.` prefix. +#[category] +pub static MATH: Category; + +// Spacings. +pub const THIN: Em = Em::new(1.0 / 6.0); +pub const MEDIUM: Em = Em::new(2.0 / 9.0); +pub const THICK: Em = Em::new(5.0 / 18.0); +pub const QUAD: Em = Em::new(1.0); +pub const WIDE: Em = Em::new(2.0); + +/// Create a module with all math definitions. +pub fn module() -> Module { + let mut math = Scope::deduplicating(); + math.category(MATH); + math.define_elem::<EquationElem>(); + math.define_elem::<TextElem>(); + math.define_elem::<LrElem>(); + math.define_elem::<MidElem>(); + math.define_elem::<AttachElem>(); + math.define_elem::<StretchElem>(); + math.define_elem::<ScriptsElem>(); + math.define_elem::<LimitsElem>(); + math.define_elem::<AccentElem>(); + math.define_elem::<UnderlineElem>(); + math.define_elem::<OverlineElem>(); + math.define_elem::<UnderbraceElem>(); + math.define_elem::<OverbraceElem>(); + math.define_elem::<UnderbracketElem>(); + math.define_elem::<OverbracketElem>(); + math.define_elem::<UnderparenElem>(); + math.define_elem::<OverparenElem>(); + math.define_elem::<UndershellElem>(); + math.define_elem::<OvershellElem>(); + math.define_elem::<CancelElem>(); + math.define_elem::<FracElem>(); + math.define_elem::<BinomElem>(); + math.define_elem::<VecElem>(); + math.define_elem::<MatElem>(); + math.define_elem::<CasesElem>(); + math.define_elem::<RootElem>(); + math.define_elem::<ClassElem>(); + math.define_elem::<OpElem>(); + math.define_elem::<PrimesElem>(); + math.define_func::<abs>(); + math.define_func::<norm>(); + math.define_func::<round>(); + math.define_func::<sqrt>(); + math.define_func::<upright>(); + math.define_func::<bold>(); + math.define_func::<italic>(); + math.define_func::<serif>(); + math.define_func::<sans>(); + math.define_func::<cal>(); + math.define_func::<frak>(); + math.define_func::<mono>(); + math.define_func::<bb>(); + math.define_func::<display>(); + math.define_func::<inline>(); + math.define_func::<script>(); + math.define_func::<sscript>(); + + // Text operators. + op::define(&mut math); + + // Spacings. + math.define("thin", HElem::new(THIN.into()).pack()); + math.define("med", HElem::new(MEDIUM.into()).pack()); + math.define("thick", HElem::new(THICK.into()).pack()); + math.define("quad", HElem::new(QUAD.into()).pack()); + math.define("wide", HElem::new(WIDE.into()).pack()); + + // Symbols. + for (name, symbol) in crate::symbols::SYM { + math.define(*name, symbol.clone()); + } + + Module::new("math", math) +} + +/// Trait for recognizing math elements and auto-wrapping them in equations. +pub trait Mathy {} + +/// A math alignment point: `&`, `&&`. +#[elem(title = "Alignment Point", Mathy)] +pub struct AlignPointElem {} + +impl AlignPointElem { + /// Get the globally shared alignment point element. + pub fn shared() -> &'static Content { + singleton!(Content, AlignPointElem::new().pack()) + } +} + +/// Forced use of a certain math class. +/// +/// This is useful to treat certain symbols as if they were of a different +/// class, e.g. to make a symbol behave like a relation. The class of a symbol +/// defines the way it is laid out, including spacing around it, and how its +/// scripts are attached by default. Note that the latter can always be +/// overridden using [`{limits}`](math.limits) and [`{scripts}`](math.scripts). +/// +/// # Example +/// ```example +/// #let loves = math.class( +/// "relation", +/// sym.suit.heart, +/// ) +/// +/// $x loves y and y loves 5$ +/// ``` +#[elem(Mathy)] +pub struct ClassElem { + /// The class to apply to the content. + #[required] + pub class: MathClass, + + /// The content to which the class is applied. + #[required] + pub body: Content, +} diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs new file mode 100644 index 00000000..ef24705a --- /dev/null +++ b/crates/typst-library/src/math/op.rs @@ -0,0 +1,103 @@ +use ecow::EcoString; + +use crate::foundations::{elem, Content, NativeElement, Scope}; +use crate::layout::HElem; +use crate::math::{upright, Mathy, THIN}; +use crate::text::TextElem; + +/// A text operator in an equation. +/// +/// # Example +/// ```example +/// $ tan x = (sin x)/(cos x) $ +/// $ op("custom", +/// limits: #true)_(n->oo) n $ +/// ``` +/// +/// # Predefined Operators { #predefined } +/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, +/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`, +/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, +/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, +/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. +#[elem(title = "Text Operator", Mathy)] +pub struct OpElem { + /// The operator's text. + #[required] + pub text: Content, + + /// Whether the operator should show attachments as limits in display mode. + #[default(false)] + pub limits: bool, +} + +macro_rules! ops { + ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => { + pub(super) fn define(math: &mut Scope) { + $({ + let operator = EcoString::from(ops!(@name $name $(: $value)?)); + math.define( + stringify!($name), + OpElem::new(TextElem::new(operator).into()) + .with_limits(ops!(@limit $($tts)*)) + .pack() + ); + })* + + let dif = |d| { + HElem::new(THIN.into()).with_weak(true).pack() + + upright(TextElem::packed(d)) + }; + math.define("dif", dif('d')); + math.define("Dif", dif('D')); + } + }; + (@name $name:ident) => { stringify!($name) }; + (@name $name:ident: $value:literal) => { $value }; + (@limit limits) => { true }; + (@limit) => { false }; +} + +ops! { + arccos, + arcsin, + arctan, + arg, + cos, + cosh, + cot, + coth, + csc, + csch, + ctg, + deg, + det (limits), + dim, + exp, + gcd (limits), + hom, + id, + im, + inf (limits), + ker, + lg, + lim (limits), + liminf: "lim inf" (limits), + limsup: "lim sup" (limits), + ln, + log, + max (limits), + min (limits), + mod, + Pr (limits), + sec, + sech, + sin, + sinc, + sinh, + sup (limits), + tan, + tanh, + tg, + tr, +} diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs new file mode 100644 index 00000000..e25c6d42 --- /dev/null +++ b/crates/typst-library/src/math/root.rs @@ -0,0 +1,35 @@ +use typst_syntax::Span; + +use crate::foundations::{elem, func, Content, NativeElement}; +use crate::math::Mathy; + +/// A square root. +/// +/// ```example +/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $ +/// ``` +#[func(title = "Square Root")] +pub fn sqrt( + /// The call span of this function. + span: Span, + /// The expression to take the square root of. + radicand: Content, +) -> Content { + RootElem::new(radicand).pack().spanned(span) +} + +/// A general root. +/// +/// ```example +/// $ root(3, x) $ +/// ``` +#[elem(Mathy)] +pub struct RootElem { + /// Which root of the radicand to take. + #[positional] + pub index: Option<Content>, + + /// The expression to take the root of. + #[required] + pub radicand: Content, +} diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs new file mode 100644 index 00000000..f3d28f2a --- /dev/null +++ b/crates/typst-library/src/math/style.rs @@ -0,0 +1,254 @@ +use crate::foundations::{func, Cast, Content, Smart}; +use crate::math::EquationElem; + +/// Bold font style in math. +/// +/// ```example +/// $ bold(A) := B^+ $ +/// ``` +#[func(keywords = ["mathbf"])] +pub fn bold( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_bold(true)) +} + +/// Upright (non-italic) font style in math. +/// +/// ```example +/// $ upright(A) != A $ +/// ``` +#[func(keywords = ["mathup"])] +pub fn upright( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_italic(Smart::Custom(false))) +} + +/// Italic font style in math. +/// +/// For roman letters and greek lowercase letters, this is already the default. +#[func(keywords = ["mathit"])] +pub fn italic( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_italic(Smart::Custom(true))) +} + +/// Serif (roman) font style in math. +/// +/// This is already the default. +#[func(keywords = ["mathrm"])] +pub fn serif( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Serif)) +} + +/// Sans-serif font style in math. +/// +/// ```example +/// $ sans(A B C) $ +/// ``` +#[func(title = "Sans Serif", keywords = ["mathsf"])] +pub fn sans( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Sans)) +} + +/// Calligraphic font style in math. +/// +/// ```example +/// Let $cal(P)$ be the set of ... +/// ``` +/// +/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these +/// styles share the same Unicode codepoints. Switching between the styles is +/// thus only possible if supported by the font via +/// [font features]($text.features). +/// +/// For the default math font, the roundhand style is available through the +/// `ss01` feature. Therefore, you could define your own version of `\mathscr` +/// like this: +/// +/// ```example +/// #let scr(it) = text( +/// features: ("ss01",), +/// box($cal(it)$), +/// ) +/// +/// We establish $cal(P) != scr(P)$. +/// ``` +/// +/// (The box is not conceptually necessary, but unfortunately currently needed +/// due to limitations in Typst's text style handling in math.) +#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] +pub fn cal( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Cal)) +} + +/// Fraktur font style in math. +/// +/// ```example +/// $ frak(P) $ +/// ``` +#[func(title = "Fraktur", keywords = ["mathfrak"])] +pub fn frak( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Frak)) +} + +/// Monospace font style in math. +/// +/// ```example +/// $ mono(x + y = z) $ +/// ``` +#[func(title = "Monospace", keywords = ["mathtt"])] +pub fn mono( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Mono)) +} + +/// Blackboard bold (double-struck) font style in math. +/// +/// For uppercase latin letters, blackboard bold is additionally available +/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`. +/// +/// ```example +/// $ bb(b) $ +/// $ bb(N) = NN $ +/// $ f: NN -> RR $ +/// ``` +#[func(title = "Blackboard Bold", keywords = ["mathbb"])] +pub fn bb( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Bb)) +} + +/// Forced display style in math. +/// +/// This is the normal size for block equations. +/// +/// ```example +/// $sum_i x_i/2 = display(sum_i x_i/2)$ +/// ``` +#[func(title = "Display Size", keywords = ["displaystyle"])] +pub fn display( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(false)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Display)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced inline (text) style in math. +/// +/// This is the normal size for inline equations. +/// +/// ```example +/// $ sum_i x_i/2 +/// = inline(sum_i x_i/2) $ +/// ``` +#[func(title = "Inline Size", keywords = ["textstyle"])] +pub fn inline( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(false)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Text)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced script style in math. +/// +/// This is the smaller size used in powers or sub- or superscripts. +/// +/// ```example +/// $sum_i x_i/2 = script(sum_i x_i/2)$ +/// ``` +#[func(title = "Script Size", keywords = ["scriptstyle"])] +pub fn script( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(true)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Script)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced second script style in math. +/// +/// This is the smallest size, used in second-level sub- and superscripts +/// (script of the script). +/// +/// ```example +/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ +/// ``` +#[func(title = "Script-Script Size", keywords = ["scriptscriptstyle"])] +pub fn sscript( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(true)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::ScriptScript)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// The size of elements in an equation. +/// +/// See the TeXbook p. 141. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)] +pub enum MathSize { + /// Second-level sub- and superscripts. + ScriptScript, + /// Sub- and superscripts. + Script, + /// Math in text. + Text, + /// Math on its own line. + Display, +} + +/// A mathematical style variant, as defined by Unicode. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)] +pub enum MathVariant { + #[default] + Serif, + Sans, + Cal, + Frak, + Mono, + Bb, +} diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs new file mode 100644 index 00000000..302c51af --- /dev/null +++ b/crates/typst-library/src/math/underover.rs @@ -0,0 +1,156 @@ +use crate::foundations::{elem, Content}; +use crate::math::Mathy; + +/// A horizontal line under content. +/// +/// ```example +/// $ underline(1 + 2 + ... + 5) $ +/// ``` +#[elem(Mathy)] +pub struct UnderlineElem { + /// The content above the line. + #[required] + pub body: Content, +} + +/// A horizontal line over content. +/// +/// ```example +/// $ overline(1 + 2 + ... + 5) $ +/// ``` +#[elem(Mathy)] +pub struct OverlineElem { + /// The content below the line. + #[required] + pub body: Content, +} + +/// A horizontal brace under content, with an optional annotation below. +/// +/// ```example +/// $ underbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderbraceElem { + /// The content above the brace. + #[required] + pub body: Content, + + /// The optional content below the brace. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal brace over content, with an optional annotation above. +/// +/// ```example +/// $ overbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverbraceElem { + /// The content below the brace. + #[required] + pub body: Content, + + /// The optional content above the brace. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal bracket under content, with an optional annotation below. +/// +/// ```example +/// $ underbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderbracketElem { + /// The content above the bracket. + #[required] + pub body: Content, + + /// The optional content below the bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal bracket over content, with an optional annotation above. +/// +/// ```example +/// $ overbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverbracketElem { + /// The content below the bracket. + #[required] + pub body: Content, + + /// The optional content above the bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal parenthesis under content, with an optional annotation below. +/// +/// ```example +/// $ underparen(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderparenElem { + /// The content above the parenthesis. + #[required] + pub body: Content, + + /// The optional content below the parenthesis. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal parenthesis over content, with an optional annotation above. +/// +/// ```example +/// $ overparen(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverparenElem { + /// The content below the parenthesis. + #[required] + pub body: Content, + + /// The optional content above the parenthesis. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal tortoise shell bracket under content, with an optional +/// annotation below. +/// +/// ```example +/// $ undershell(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UndershellElem { + /// The content above the tortoise shell bracket. + #[required] + pub body: Content, + + /// The optional content below the tortoise shell bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal tortoise shell bracket over content, with an optional +/// annotation above. +/// +/// ```example +/// $ overshell(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OvershellElem { + /// The content below the tortoise shell bracket. + #[required] + pub body: Content, + + /// The optional content above the tortoise shell bracket. + #[positional] + pub annotation: Option<Content>, +} |
