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/equation.rs | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/math/equation.rs')
| -rw-r--r-- | crates/typst-library/src/math/equation.rs | 256 |
1 files changed, 256 insertions, 0 deletions
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)) + } +} |
