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