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    ///   $ integral_1^oo a x^2 + b med d x $,
115    /// )
116    /// ```
117    pub alt: Option<EcoString>,
118
119    /// The contents of the equation.
120    #[required]
121    pub body: Content,
122
123    /// The size of the glyphs.
124    #[internal]
125    #[default(MathSize::Text)]
126    #[ghost]
127    pub size: MathSize,
128
129    /// The style variant to select.
130    #[internal]
131    #[ghost]
132    pub variant: Option<MathVariant>,
133
134    /// Affects the height of exponents.
135    #[internal]
136    #[default(false)]
137    #[ghost]
138    pub cramped: bool,
139
140    /// Whether to use bold glyphs.
141    #[internal]
142    #[default(false)]
143    #[ghost]
144    pub bold: bool,
145
146    /// Whether to use italic glyphs.
147    #[internal]
148    #[ghost]
149    pub italic: Option<bool>,
150
151    /// A forced class to use for all fragment.
152    #[internal]
153    #[ghost]
154    pub class: Option<MathClass>,
155
156    /// Values of `scriptPercentScaleDown` and `scriptScriptPercentScaleDown`
157    /// respectively in the current font's MathConstants table.
158    #[internal]
159    #[default((70, 50))]
160    #[ghost]
161    pub script_scale: (i16, i16),
162
163    /// The locale of this element (used for the alternative description).
164    #[internal]
165    #[synthesized]
166    pub locale: Locale,
167}
168
169impl Synthesize for Packed<EquationElem> {
170    fn synthesize(
171        &mut self,
172        engine: &mut Engine,
173        styles: StyleChain,
174    ) -> SourceResult<()> {
175        let supplement = match self.as_ref().supplement.get_ref(styles) {
176            Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
177            Smart::Custom(None) => Content::empty(),
178            Smart::Custom(Some(supplement)) => {
179                supplement.resolve(engine, styles, [self.clone().pack()])?
180            }
181        };
182
183        self.supplement
184            .set(Smart::Custom(Some(Supplement::Content(supplement))));
185
186        self.locale = Some(Locale::get_in(styles));
187
188        Ok(())
189    }
190}
191
192impl ShowSet for Packed<EquationElem> {
193    fn show_set(&self, styles: StyleChain) -> Styles {
194        let mut out = Styles::new();
195        if self.block.get(styles) {
196            out.set(AlignElem::alignment, Alignment::CENTER);
197            out.set(BlockElem::breakable, false);
198            out.set(ParLine::numbering, None);
199            out.set(EquationElem::size, MathSize::Display);
200        } else {
201            out.set(EquationElem::size, MathSize::Text);
202        }
203        out.set(TextElem::weight, FontWeight::from_number(450));
204        out.set(
205            TextElem::font,
206            FontList(vec![FontFamily::new("New Computer Modern Math")]),
207        );
208        out
209    }
210}
211
212impl Count for Packed<EquationElem> {
213    fn update(&self) -> Option<CounterUpdate> {
214        (self.block.get(StyleChain::default()) && self.numbering().is_some())
215            .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
216    }
217}
218
219impl LocalName for Packed<EquationElem> {
220    const KEY: &'static str = "equation";
221}
222
223impl Refable for Packed<EquationElem> {
224    fn supplement(&self) -> Content {
225        // After synthesis, this should always be custom content.
226        match self.supplement.get_cloned(StyleChain::default()) {
227            Smart::Custom(Some(Supplement::Content(content))) => content,
228            _ => Content::empty(),
229        }
230    }
231
232    fn counter(&self) -> Counter {
233        Counter::of(EquationElem::ELEM)
234    }
235
236    fn numbering(&self) -> Option<&Numbering> {
237        self.numbering.get_ref(StyleChain::default()).as_ref()
238    }
239}
240
241impl Outlinable for Packed<EquationElem> {
242    fn outlined(&self) -> bool {
243        self.block.get(StyleChain::default()) && self.numbering().is_some()
244    }
245
246    fn prefix(&self, numbers: Content) -> Content {
247        let supplement = self.supplement();
248        if !supplement.is_empty() {
249            supplement + TextElem::packed('\u{a0}') + numbers
250        } else {
251            numbers
252        }
253    }
254
255    fn body(&self) -> Content {
256        Content::empty()
257    }
258}