Skip to main content

typst_library/math/
equation.rs

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