typst_library/math/
equation.rs

1use std::num::NonZeroUsize;
2
3use codex::styling::MathVariant;
4use ecow::EcoString;
5use typst_utils::NonZeroExt;
6use unicode_math_class::MathClass;
7
8use crate::diag::SourceResult;
9use crate::engine::Engine;
10use crate::foundations::{
11    Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, elem,
12};
13use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Tagged};
14use crate::layout::{
15    AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment,
16};
17use crate::math::MathSize;
18use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
19use crate::text::{FontFamily, FontList, FontWeight, LocalName, Locale, 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 whitespace after the opening
25/// dollar sign and whitespace 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
46/// whitespace lifts it into a separate block that is centered horizontally.
47/// For more details about math syntax, see the
48/// [main math page]($category/math).
49#[elem(Locatable, Tagged, Synthesize, 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 block-level equations. Accepts a
56    /// [numbering pattern or function]($numbering) taking a single number.
57    ///
58    /// ```example
59    /// #set math.equation(numbering: "(1)")
60    ///
61    /// We define:
62    /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
63    ///
64    /// With @ratio, we get:
65    /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
66    /// ```
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    /// An alternative description of the mathematical equation.
105    ///
106    /// This should describe the full equation in natural language and will be
107    /// made available to Assistive Technology. You can learn more in the
108    /// [Textual Representations section of the Accessibility
109    /// Guide]($guides/accessibility/#textual-representations).
110    ///
111    /// ```example
112    /// #math.equation(
113    ///   alt: "integral from 1 to infinity of a x squared plus b with respect to x",
114    ///   block: true,
115    ///   $ integral_1^oo a x^2 + b dif x $,
116    /// )
117    /// ```
118    pub alt: Option<EcoString>,
119
120    /// The contents of the equation.
121    #[required]
122    pub body: Content,
123
124    /// The size of the glyphs.
125    #[internal]
126    #[default(MathSize::Text)]
127    #[ghost]
128    pub size: MathSize,
129
130    /// The style variant to select.
131    #[internal]
132    #[ghost]
133    pub variant: Option<MathVariant>,
134
135    /// Affects the height of exponents.
136    #[internal]
137    #[default(false)]
138    #[ghost]
139    pub cramped: bool,
140
141    /// Whether to use bold glyphs.
142    #[internal]
143    #[default(false)]
144    #[ghost]
145    pub bold: bool,
146
147    /// Whether to use italic glyphs.
148    #[internal]
149    #[ghost]
150    pub italic: Option<bool>,
151
152    /// A forced class to use for all fragment.
153    #[internal]
154    #[ghost]
155    pub class: Option<MathClass>,
156
157    /// Values of `scriptPercentScaleDown` and `scriptScriptPercentScaleDown`
158    /// respectively in the current font's MathConstants table.
159    #[internal]
160    #[default((70, 50))]
161    #[ghost]
162    pub script_scale: (i16, i16),
163
164    /// The locale of this element (used for the alternative description).
165    #[internal]
166    #[synthesized]
167    pub locale: Locale,
168}
169
170impl Synthesize for Packed<EquationElem> {
171    fn synthesize(
172        &mut self,
173        engine: &mut Engine,
174        styles: StyleChain,
175    ) -> SourceResult<()> {
176        let supplement = match self.as_ref().supplement.get_ref(styles) {
177            Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
178            Smart::Custom(None) => Content::empty(),
179            Smart::Custom(Some(supplement)) => {
180                supplement.resolve(engine, styles, [self.clone().pack()])?
181            }
182        };
183
184        self.supplement
185            .set(Smart::Custom(Some(Supplement::Content(supplement))));
186
187        self.locale = Some(Locale::get_in(styles));
188
189        Ok(())
190    }
191}
192
193impl ShowSet for Packed<EquationElem> {
194    fn show_set(&self, styles: StyleChain) -> Styles {
195        let mut out = Styles::new();
196        if self.block.get(styles) {
197            out.set(AlignElem::alignment, Alignment::CENTER);
198            out.set(BlockElem::breakable, false);
199            out.set(ParLine::numbering, None);
200            out.set(EquationElem::size, MathSize::Display);
201        } else {
202            out.set(EquationElem::size, MathSize::Text);
203        }
204        out.set(TextElem::weight, FontWeight::from_number(450));
205        out.set(
206            TextElem::font,
207            FontList(vec![FontFamily::new("New Computer Modern Math")]),
208        );
209        out
210    }
211}
212
213impl Count for Packed<EquationElem> {
214    fn update(&self) -> Option<CounterUpdate> {
215        (self.block.get(StyleChain::default()) && self.numbering().is_some())
216            .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
217    }
218}
219
220impl LocalName for Packed<EquationElem> {
221    const KEY: &'static str = "equation";
222}
223
224impl Refable for Packed<EquationElem> {
225    fn supplement(&self) -> Content {
226        // After synthesis, this should always be custom content.
227        match self.supplement.get_cloned(StyleChain::default()) {
228            Smart::Custom(Some(Supplement::Content(content))) => content,
229            _ => Content::empty(),
230        }
231    }
232
233    fn counter(&self) -> Counter {
234        Counter::of(EquationElem::ELEM)
235    }
236
237    fn numbering(&self) -> Option<&Numbering> {
238        self.numbering.get_ref(StyleChain::default()).as_ref()
239    }
240}
241
242impl Outlinable for Packed<EquationElem> {
243    fn outlined(&self) -> bool {
244        self.block.get(StyleChain::default()) && self.numbering().is_some()
245    }
246
247    fn prefix(&self, numbers: Content) -> Content {
248        let supplement = self.supplement();
249        if !supplement.is_empty() {
250            supplement + TextElem::packed('\u{a0}') + numbers
251        } else {
252            numbers
253        }
254    }
255
256    fn body(&self) -> Content {
257        Content::empty()
258    }
259}