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}