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>`</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>⃛</code> |
47 /// | Quadruple dot | `dot.quad` | <code>⃜</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}