typst_library/math/
accent.rs

1use std::sync::LazyLock;
2
3use icu_properties::CanonicalCombiningClass;
4use icu_properties::maps::CodePointMapData;
5use icu_provider::AsDeserializingBufferProvider;
6use icu_provider_blob::BlobDataProvider;
7
8use crate::diag::bail;
9use crate::foundations::{Content, NativeElement, SymbolElem, cast, elem, func};
10use crate::layout::{Length, Rel};
11use crate::math::Mathy;
12
13/// Attaches an accent to a base.
14///
15/// # Example
16/// ```example
17/// $grave(a) = accent(a, `)$ \
18/// $arrow(a) = accent(a, arrow)$ \
19/// $tilde(a) = accent(a, \u{0303})$
20/// ```
21#[elem(Mathy)]
22pub struct AccentElem {
23    /// The base to which the accent is applied. May consist of multiple
24    /// letters.
25    ///
26    /// ```example
27    /// $arrow(A B C)$
28    /// ```
29    #[required]
30    pub base: Content,
31
32    /// The accent to apply to the base.
33    ///
34    /// Supported accents include:
35    ///
36    /// | Accent        | Name            | Codepoint |
37    /// | ------------- | --------------- | --------- |
38    /// | Grave         | `grave`         | <code>&DiacriticalGrave;</code> |
39    /// | Acute         | `acute`         | `´`       |
40    /// | Circumflex    | `hat`           | `^`       |
41    /// | Tilde         | `tilde`         | `~`       |
42    /// | Macron        | `macron`        | `¯`       |
43    /// | Dash          | `dash`          | `‾`       |
44    /// | Breve         | `breve`         | `˘`       |
45    /// | Dot           | `dot`           | `.`       |
46    /// | Double dot, Diaeresis | `dot.double`, `diaer` | `¨` |
47    /// | Triple dot    | `dot.triple`    | <code>&tdot;</code> |
48    /// | Quadruple dot | `dot.quad`      | <code>&DotDot;</code> |
49    /// | Circle        | `circle`        | `∘`       |
50    /// | Double acute  | `acute.double`  | `˝`       |
51    /// | Caron         | `caron`         | `ˇ`       |
52    /// | Right arrow   | `arrow`, `->`   | `→`       |
53    /// | Left arrow    | `arrow.l`, `<-` | `←`       |
54    /// | Left/Right arrow | `arrow.l.r`  | `↔`       |
55    /// | Right harpoon | `harpoon`       | `⇀`       |
56    /// | Left harpoon  | `harpoon.lt`    | `↼`       |
57    #[required]
58    pub accent: Accent,
59
60    /// The size of the accent, relative to the width of the base.
61    ///
62    /// ```example
63    /// $dash(A, size: #150%)$
64    /// ```
65    #[default(Rel::one())]
66    pub size: Rel<Length>,
67
68    /// Whether to remove the dot on top of lowercase i and j when adding a top
69    /// accent.
70    ///
71    /// This enables the `dtls` OpenType feature.
72    ///
73    /// ```example
74    /// $hat(dotless: #false, i)$
75    /// ```
76    #[default(true)]
77    pub dotless: bool,
78}
79
80/// An accent character.
81#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
82pub struct Accent(pub char);
83
84impl Accent {
85    /// Normalize a character into an accent.
86    pub fn new(c: char) -> Self {
87        Self(Self::combine(c).unwrap_or(c))
88    }
89
90    /// Whether this accent is a bottom accent or not.
91    pub fn is_bottom(&self) -> bool {
92        static COMBINING_CLASS_DATA: LazyLock<CodePointMapData<CanonicalCombiningClass>> =
93            LazyLock::new(|| {
94                icu_properties::maps::load_canonical_combining_class(
95                    &BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU)
96                        .unwrap()
97                        .as_deserializing(),
98                )
99                .unwrap()
100            });
101
102        matches!(
103            COMBINING_CLASS_DATA.as_borrowed().get(self.0),
104            CanonicalCombiningClass::Below
105        )
106    }
107}
108
109/// This macro generates accent-related functions.
110///
111/// ```ignore
112/// accents! {
113///     '\u{0300}' | '`' => grave,
114/// //  ^^^^^^^^^    ^^^    ^^^^^
115/// //  |            |      |
116/// //  |            |      +-- The name of the function.
117/// //  |            +--------- The alternative characters that represent the accent.
118/// //  +---------------------- The primary character that represents the accent.
119/// }
120/// ```
121///
122/// When combined with the `Accent::combine` function, accent characters can be normalized
123/// to the primary character.
124macro_rules! accents {
125    ($($primary:literal $(| $alt:literal)* => $name:ident),* $(,)?) => {
126        impl Accent {
127            /// Normalize an accent to a combining one.
128            pub fn combine(c: char) -> Option<char> {
129                Some(match c {
130                    $($primary $(| $alt)* => $primary,)*
131                    _ => return None,
132                })
133            }
134        }
135
136        $(
137            /// The accent function for callable symbol definitions.
138            #[func]
139            pub fn $name(
140                /// The base to which the accent is applied.
141                base: Content,
142                /// The size of the accent, relative to the width of the base.
143                #[named]
144                size: Option<Rel<Length>>,
145                /// Whether to remove the dot on top of lowercase i and j when
146                /// adding a top accent.
147                #[named]
148                dotless: Option<bool>,
149            ) -> Content {
150                let mut accent = AccentElem::new(base, Accent::new($primary));
151                if let Some(size) = size {
152                    accent = accent.with_size(size);
153                }
154                if let Some(dotless) = dotless {
155                    accent = accent.with_dotless(dotless);
156                }
157                accent.pack()
158            }
159        )+
160    };
161}
162
163// Keep it synced with the documenting table above.
164accents! {
165    '\u{0300}' | '`' => grave,
166    '\u{0301}' | '´' => acute,
167    '\u{0302}' | '^' | 'ˆ' => hat,
168    '\u{0303}' | '~' | '∼' | '˜' => tilde,
169    '\u{0304}' | '¯' => macron,
170    '\u{0305}' | '-' | '‾' | '−' => dash,
171    '\u{0306}' | '˘' => breve,
172    '\u{0307}' | '.' | '˙' | '⋅' => dot,
173    '\u{0308}' | '¨' => dot_double,
174    '\u{20db}' => dot_triple,
175    '\u{20dc}' => dot_quad,
176    '\u{030a}' | '∘' | '○' => circle,
177    '\u{030b}' | '˝' => acute_double,
178    '\u{030c}' | 'ˇ' => caron,
179    '\u{20d6}' | '←' => arrow_l,
180    '\u{20d7}' | '→' | '⟶' => arrow,
181    '\u{20e1}' | '↔' | '⟷' => arrow_l_r,
182    '\u{20d0}' | '↼' => harpoon_lt,
183    '\u{20d1}' | '⇀' => harpoon,
184}
185
186cast! {
187    Accent,
188    self => self.0.into_value(),
189    v: char => Self::new(v),
190    v: Content => match v.to_packed::<SymbolElem>().and_then(|elem| elem.text.parse::<char>().ok()) {
191        Some(c) => Self::new(c),
192        _ => bail!("expected a single-codepoint symbol"),
193    },
194}