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}