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