Skip to main content

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}