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}