typst_library/math/
equation.rs

1use std::num::NonZeroUsize;
2
3use typst_utils::NonZeroExt;
4use unicode_math_class::MathClass;
5
6use crate::diag::SourceResult;
7use crate::engine::Engine;
8use crate::foundations::{
9    elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
10    Synthesize,
11};
12use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
13use crate::layout::{
14    AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment,
15    VAlignment,
16};
17use crate::math::{MathSize, MathVariant};
18use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
19use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
20
21/// A mathematical equation.
22///
23/// Can be displayed inline with text or as a separate block. An equation
24/// becomes block-level through the presence of at least one space after the
25/// opening dollar sign and one space before the closing dollar sign.
26///
27/// # Example
28/// ```example
29/// #set text(font: "New Computer Modern")
30///
31/// Let $a$, $b$, and $c$ be the side
32/// lengths of right-angled triangle.
33/// Then, we know that:
34/// $ a^2 + b^2 = c^2 $
35///
36/// Prove by induction:
37/// $ sum_(k=1)^n k = (n(n+1)) / 2 $
38/// ```
39///
40/// By default, block-level equations will not break across pages. This can be
41/// changed through `{show math.equation: set block(breakable: true)}`.
42///
43/// # Syntax
44/// This function also has dedicated syntax: Write mathematical markup within
45/// dollar signs to create an equation. Starting and ending the equation with at
46/// least one space lifts it into a separate block that is centered
47/// horizontally. For more details about math syntax, see the
48/// [main math page]($category/math).
49#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)]
50pub struct EquationElem {
51    /// Whether the equation is displayed as a separate block.
52    #[default(false)]
53    pub block: bool,
54
55    /// How to [number]($numbering) block-level equations.
56    ///
57    /// ```example
58    /// #set math.equation(numbering: "(1)")
59    ///
60    /// We define:
61    /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
62    ///
63    /// With @ratio, we get:
64    /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
65    /// ```
66    #[borrowed]
67    pub numbering: Option<Numbering>,
68
69    /// The alignment of the equation numbering.
70    ///
71    /// By default, the alignment is `{end + horizon}`. For the horizontal
72    /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}`
73    /// of the text direction; for the vertical component, you can use
74    /// `{top}`, `{horizon}`, or `{bottom}`.
75    ///
76    /// ```example
77    /// #set math.equation(numbering: "(1)", number-align: bottom)
78    ///
79    /// We can calculate:
80    /// $ E &= sqrt(m_0^2 + p^2) \
81    ///     &approx 125 "GeV" $
82    /// ```
83    #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))]
84    pub number_align: SpecificAlignment<OuterHAlignment, VAlignment>,
85
86    /// A supplement for the equation.
87    ///
88    /// For references to equations, this is added before the referenced number.
89    ///
90    /// If a function is specified, it is passed the referenced equation and
91    /// should return content.
92    ///
93    /// ```example
94    /// #set math.equation(numbering: "(1)", supplement: [Eq.])
95    ///
96    /// We define:
97    /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
98    ///
99    /// With @ratio, we get:
100    /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
101    /// ```
102    pub supplement: Smart<Option<Supplement>>,
103
104    /// The contents of the equation.
105    #[required]
106    pub body: Content,
107
108    /// The size of the glyphs.
109    #[internal]
110    #[default(MathSize::Text)]
111    #[ghost]
112    pub size: MathSize,
113
114    /// The style variant to select.
115    #[internal]
116    #[ghost]
117    pub variant: MathVariant,
118
119    /// Affects the height of exponents.
120    #[internal]
121    #[default(false)]
122    #[ghost]
123    pub cramped: bool,
124
125    /// Whether to use bold glyphs.
126    #[internal]
127    #[default(false)]
128    #[ghost]
129    pub bold: bool,
130
131    /// Whether to use italic glyphs.
132    #[internal]
133    #[ghost]
134    pub italic: Smart<bool>,
135
136    /// A forced class to use for all fragment.
137    #[internal]
138    #[ghost]
139    pub class: Option<MathClass>,
140
141    /// Values of `scriptPercentScaleDown` and `scriptScriptPercentScaleDown`
142    /// respectively in the current font's MathConstants table.
143    #[internal]
144    #[default((70, 50))]
145    #[ghost]
146    pub script_scale: (i16, i16),
147}
148
149impl Synthesize for Packed<EquationElem> {
150    fn synthesize(
151        &mut self,
152        engine: &mut Engine,
153        styles: StyleChain,
154    ) -> SourceResult<()> {
155        let supplement = match self.as_ref().supplement(styles) {
156            Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
157            Smart::Custom(None) => Content::empty(),
158            Smart::Custom(Some(supplement)) => {
159                supplement.resolve(engine, styles, [self.clone().pack()])?
160            }
161        };
162
163        self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
164        Ok(())
165    }
166}
167
168impl Show for Packed<EquationElem> {
169    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
170        if self.block(styles) {
171            Ok(BlockElem::multi_layouter(
172                self.clone(),
173                engine.routines.layout_equation_block,
174            )
175            .pack()
176            .spanned(self.span()))
177        } else {
178            Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline)
179                .pack()
180                .spanned(self.span()))
181        }
182    }
183}
184
185impl ShowSet for Packed<EquationElem> {
186    fn show_set(&self, styles: StyleChain) -> Styles {
187        let mut out = Styles::new();
188        if self.block(styles) {
189            out.set(AlignElem::set_alignment(Alignment::CENTER));
190            out.set(BlockElem::set_breakable(false));
191            out.set(ParLine::set_numbering(None));
192            out.set(EquationElem::set_size(MathSize::Display));
193        } else {
194            out.set(EquationElem::set_size(MathSize::Text));
195        }
196        out.set(TextElem::set_weight(FontWeight::from_number(450)));
197        out.set(TextElem::set_font(FontList(vec![FontFamily::new(
198            "New Computer Modern Math",
199        )])));
200        out
201    }
202}
203
204impl Count for Packed<EquationElem> {
205    fn update(&self) -> Option<CounterUpdate> {
206        (self.block(StyleChain::default()) && self.numbering().is_some())
207            .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
208    }
209}
210
211impl LocalName for Packed<EquationElem> {
212    const KEY: &'static str = "equation";
213}
214
215impl Refable for Packed<EquationElem> {
216    fn supplement(&self) -> Content {
217        // After synthesis, this should always be custom content.
218        match (**self).supplement(StyleChain::default()) {
219            Smart::Custom(Some(Supplement::Content(content))) => content,
220            _ => Content::empty(),
221        }
222    }
223
224    fn counter(&self) -> Counter {
225        Counter::of(EquationElem::elem())
226    }
227
228    fn numbering(&self) -> Option<&Numbering> {
229        (**self).numbering(StyleChain::default()).as_ref()
230    }
231}
232
233impl Outlinable for Packed<EquationElem> {
234    fn outlined(&self) -> bool {
235        self.block(StyleChain::default()) && self.numbering().is_some()
236    }
237
238    fn prefix(&self, numbers: Content) -> Content {
239        let supplement = self.supplement();
240        if !supplement.is_empty() {
241            supplement + TextElem::packed('\u{a0}') + numbers
242        } else {
243            numbers
244        }
245    }
246
247    fn body(&self) -> Content {
248        Content::empty()
249    }
250}