summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math/equation.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/math/equation.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (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.rs256
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))
+ }
+}