typst_library/math/accent.rs
1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4use bumpalo::Bump;
5use comemo::Tracked;
6use icu_properties::CodePointMapDataBorrowed;
7use icu_properties::props::CanonicalCombiningClass;
8
9use crate::engine::Engine;
10use crate::foundations::{
11 Args, CastInfo, Content, Context, Func, IntoValue, NativeElement, NativeFuncData,
12 NativeFuncPtr, NativeParamInfo, Reflect, Scope, Str, SymbolElem, Type, cast, elem,
13};
14use crate::layout::{Em, Length, Rel};
15use crate::math::Mathy;
16
17/// How much the accent can be shorter than the base.
18pub const ACCENT_SHORT_FALL: Em = Em::new(0.5);
19
20/// Attaches an accent to a base.
21///
22/// In math mode, common accents are also available as named @symbol[symbols]
23/// that can be directly called (like @function[functions]) to attach them to
24/// some content.
25///
26/// = Example <example>
27/// ```example
28/// $grave(a) = accent(a, `)$ \
29/// $arrow(a) = accent(a, arrow)$ \
30/// $tilde(a) = accent(a, \u{0303})$
31/// ```
32#[elem(Mathy)]
33pub struct AccentElem {
34 /// The base to which the accent is applied. May consist of multiple
35 /// letters.
36 ///
37 /// ```example
38 /// $arrow(A B C)$
39 /// ```
40 #[required]
41 pub base: Content,
42
43 /// The accent to apply to the base.
44 ///
45 /// Supported accents include:
46 ///
47 /// #docs-table(
48 /// table.header[Accent][Name][Codepoint],
49 ///
50 /// [Grave],
51 /// [`grave`],
52 /// [``` ` ```],
53 ///
54 /// [Acute],
55 /// [`acute`],
56 /// [`´`],
57 ///
58 /// [Circumflex],
59 /// [`hat`],
60 /// [`^`],
61 ///
62 /// [Tilde],
63 /// [`tilde`],
64 /// [`~`],
65 ///
66 /// [Macron],
67 /// [`macron`],
68 /// [`¯`],
69 ///
70 /// [Dash],
71 /// [`dash`],
72 /// [`‾`],
73 ///
74 /// [Breve],
75 /// [`breve`],
76 /// [`˘`],
77 ///
78 /// [Dot],
79 /// [`dot`],
80 /// [`.`],
81 ///
82 /// [Double dot, Diaeresis],
83 /// [`dot.double`, `diaer`],
84 /// [`¨`],
85 ///
86 /// [Triple dot],
87 /// [`dot.triple`],
88 /// raw(lang: "typ", "\\u{20db}"),
89 ///
90 /// [Quadruple dot],
91 /// [`dot.quad`],
92 /// raw(lang: "typ", "\\u{20dc}"),
93 ///
94 /// [Circle],
95 /// [`circle`],
96 /// [`∘`],
97 ///
98 /// [Double acute],
99 /// [`acute.double`],
100 /// [`˝`],
101 ///
102 /// [Caron],
103 /// [`caron`],
104 /// [`ˇ`],
105 ///
106 /// [Right arrow],
107 /// [`arrow`, `->`],
108 /// [`→`],
109 ///
110 /// [Left arrow],
111 /// [`arrow.l`, `<-`],
112 /// [`←`],
113 ///
114 /// [Left/Right arrow],
115 /// [`arrow.l.r`],
116 /// [`↔`],
117 ///
118 /// [Right harpoon],
119 /// [`harpoon`],
120 /// [`⇀`],
121 ///
122 /// [Left harpoon],
123 /// [`harpoon.lt`],
124 /// [`↼`],
125 /// )
126 #[required]
127 pub accent: Accent,
128
129 /// The size of the accent, relative to the width of the base.
130 ///
131 /// #example(
132 /// title: "Basic usage",
133 /// ```
134 /// $dash(A, size: #150%)$
135 /// ```
136 /// )
137 ///
138 /// Note that the resulting accent may not have the exact desired size. For
139 /// example, an arrow may be either a pre-defined short glyph, or a long
140 /// glyph assembled from building blocks (arrowhead + line) provided by the
141 /// font. The sizes of the two possibilities may not cover the entire span.
142 /// Consequently, arrows of certain intermediate sizes cannot be
143 /// constructed.
144 ///
145 /// #example(
146 /// title: "Size of arrow growing discontinuously",
147 /// ```
148 /// >>> #set par(spacing: 0.3em)
149 /// #for i in range(6) {
150 /// $ arrow(#box(
151 /// width: 0.4em + 0.3em * i,
152 /// fill: aqua,
153 /// height: 0.4em,
154 /// )) $
155 /// }
156 /// ```
157 /// )
158 #[default(Rel::one())]
159 pub size: Rel<Length>,
160
161 /// Whether to remove the dot on top of lowercase i and j when adding a top
162 /// accent.
163 ///
164 /// This enables the `dtls` OpenType feature.
165 ///
166 /// ```example
167 /// $hat(dotless: #false, i)$
168 /// ```
169 #[default(true)]
170 pub dotless: bool,
171}
172
173/// An accent character.
174#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
175pub struct Accent(pub char);
176
177impl Accent {
178 /// Tries to select the appropriate combining accent for a string, falling
179 /// back to the string's lone character if there is no corresponding one.
180 ///
181 /// Returns `None` if there isn't one and the string has more than one
182 /// character.
183 pub fn normalize(s: &str) -> Option<Self> {
184 Self::combining(s).or_else(|| s.parse::<char>().ok().map(Self))
185 }
186
187 /// Tries to select a well-known combining accent that matches for the
188 /// value.
189 pub fn combining(value: &str) -> Option<Self> {
190 let c = value.parse::<char>().ok();
191 ACCENTS
192 .iter()
193 .copied()
194 .find(|&(accent, names)| Some(accent) == c || names.contains(&value))
195 .map(|(accent, _)| Self(accent))
196 }
197
198 /// Whether this accent is a bottom accent or not.
199 pub fn is_bottom(&self) -> bool {
200 if matches!(self.0, '⏟' | '⎵' | '⏝' | '⏡') {
201 return true;
202 }
203
204 const COMBINING_CLASS_DATA: CodePointMapDataBorrowed<CanonicalCombiningClass> =
205 CodePointMapDataBorrowed::new();
206
207 matches!(COMBINING_CLASS_DATA.get(self.0), CanonicalCombiningClass::Below)
208 }
209}
210
211/// Gets the accent function corresponding to a symbol value, if any.
212pub fn get_accent_func(value: &str) -> Option<Func> {
213 Accent::combining(value).map(|accent| (&FUNCS[&accent]).into())
214}
215
216// Keep it synced with the documenting table above and the
217// `math-accent-sym-call` test.`
218/// A list of accents, each with a list of alternative names.
219const ACCENTS: &[(char, &[&str])] = &[
220 // Note: Symbols that can have a text presentation must explicitly have that
221 // alternative listed here.
222 ('\u{0300}', &["`"]),
223 ('\u{0301}', &["´"]),
224 ('\u{0302}', &["^", "ˆ"]),
225 ('\u{0303}', &["~", "∼", "˜"]),
226 ('\u{0304}', &["¯"]),
227 ('\u{0305}', &["-", "–", "‾", "−"]),
228 ('\u{0306}', &["˘"]),
229 ('\u{0307}', &[".", "˙", "⋅"]),
230 ('\u{0308}', &["¨"]),
231 ('\u{20db}', &[]),
232 ('\u{20dc}', &[]),
233 ('\u{030a}', &["∘", "○"]),
234 ('\u{030b}', &["˝"]),
235 ('\u{030c}', &["ˇ"]),
236 ('\u{20d6}', &["←"]),
237 ('\u{20d7}', &["→", "⟶"]),
238 ('\u{20e1}', &["↔", "↔\u{fe0e}", "⟷"]),
239 ('\u{20d0}', &["↼"]),
240 ('\u{20d1}', &["⇀"]),
241];
242
243/// Lazily created accent functions.
244static FUNCS: LazyLock<HashMap<Accent, NativeFuncData>> = LazyLock::new(|| {
245 let bump = Box::leak(Box::new(Bump::new()));
246 ACCENTS
247 .iter()
248 .copied()
249 .map(|(accent, _)| (Accent(accent), create_accent_func_data(accent, bump)))
250 .collect()
251});
252
253/// Creates metadata for an accent wrapper function.
254fn create_accent_func_data(accent: char, bump: &'static Bump) -> NativeFuncData {
255 let title = bumpalo::format!(in bump, "Accent ({})", accent).into_bump_str();
256 let docs = bumpalo::format!(in bump, "Adds the accent {} on an expression.", accent)
257 .into_bump_str();
258 NativeFuncData {
259 function: NativeFuncPtr(bump.alloc(
260 move |_: &mut Engine, _: Tracked<Context>, args: &mut Args| {
261 let base = args.expect("base")?;
262 let size = args.named("size")?;
263 let dotless = args.named("dotless")?;
264 let mut elem = AccentElem::new(base, Accent(accent));
265 if let Some(size) = size {
266 elem = elem.with_size(size);
267 }
268 if let Some(dotless) = dotless {
269 elem = elem.with_dotless(dotless);
270 }
271 Ok(elem.pack().into_value())
272 },
273 )),
274 name: "(..) => ..",
275 title,
276 docs,
277 def_site: None,
278 keywords: &[],
279 contextual: false,
280 scope: LazyLock::new(&|| Scope::new()),
281 params: LazyLock::new(&|| create_accent_param_info()),
282 returns: LazyLock::new(&|| CastInfo::Type(Type::of::<Content>())),
283 }
284}
285
286/// Creates parameter signature metadata for an accent function.
287fn create_accent_param_info() -> Vec<NativeParamInfo> {
288 vec![
289 NativeParamInfo {
290 name: "base",
291 docs: "The base to which the accent is applied.",
292 def_site: None,
293 input: Content::input(),
294 default: None,
295 positional: true,
296 named: false,
297 variadic: false,
298 required: true,
299 settable: false,
300 },
301 NativeParamInfo {
302 name: "size",
303 docs: "The size of the accent, relative to the width of the base.",
304 def_site: None,
305 input: Rel::<Length>::input(),
306 default: None,
307 positional: false,
308 named: true,
309 variadic: false,
310 required: false,
311 settable: false,
312 },
313 NativeParamInfo {
314 name: "dotless",
315 docs: "Whether to remove the dot on top of lowercase i and j when adding a top accent.",
316 def_site: None,
317 input: bool::input(),
318 default: None,
319 positional: false,
320 named: true,
321 variadic: false,
322 required: false,
323 settable: false,
324 },
325 ]
326}
327
328cast! {
329 Accent,
330 self => self.0.into_value(),
331 // The string cast handles
332 // - strings: `accent(a, "↔")`
333 // - symbol values: `accent(a, <->)`
334 // - shorthands: `accent(a, arrow.l.r)`
335 v: Str => Self::normalize(&v).ok_or("expected exactly one character")?,
336 // The content cast is for accent uses like `accent(a, ↔)`
337 v: Content => v.to_packed::<SymbolElem>()
338 .and_then(|elem| Accent::normalize(&elem.text))
339 .ok_or("expected a single-codepoint symbol")?,
340}