1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5pub(crate) struct GlyphInfo {
6 pub index: u8,
7 pub hex_char: char,
8 pub name: &'static str,
9 pub emoji: &'static str,
10}
11
12pub(crate) const GLYPH_TABLE: [GlyphInfo; 16] = [
13 GlyphInfo {
14 index: 0,
15 hex_char: '0',
16 name: "Sunset",
17 emoji: "\u{1F305}",
18 },
19 GlyphInfo {
20 index: 1,
21 hex_char: '1',
22 name: "Bird",
23 emoji: "\u{1F54A}\u{FE0F}",
24 },
25 GlyphInfo {
26 index: 2,
27 hex_char: '2',
28 name: "Face",
29 emoji: "\u{1F611}",
30 },
31 GlyphInfo {
32 index: 3,
33 hex_char: '3',
34 name: "Diplo",
35 emoji: "\u{1F995}",
36 },
37 GlyphInfo {
38 index: 4,
39 hex_char: '4',
40 name: "Eclipse",
41 emoji: "\u{1F31C}",
42 },
43 GlyphInfo {
44 index: 5,
45 hex_char: '5',
46 name: "Balloon",
47 emoji: "\u{1F388}",
48 },
49 GlyphInfo {
50 index: 6,
51 hex_char: '6',
52 name: "Boat",
53 emoji: "\u{26F5}",
54 },
55 GlyphInfo {
56 index: 7,
57 hex_char: '7',
58 name: "Bug",
59 emoji: "\u{1F41C}",
60 },
61 GlyphInfo {
62 index: 8,
63 hex_char: '8',
64 name: "Dragonfly",
65 emoji: "\u{1F98B}",
66 },
67 GlyphInfo {
68 index: 9,
69 hex_char: '9',
70 name: "Galaxy",
71 emoji: "\u{1F300}",
72 },
73 GlyphInfo {
74 index: 10,
75 hex_char: 'A',
76 name: "Voxel",
77 emoji: "\u{1F54B}",
78 },
79 GlyphInfo {
80 index: 11,
81 hex_char: 'B',
82 name: "Whale",
83 emoji: "\u{1F40B}",
84 },
85 GlyphInfo {
86 index: 12,
87 hex_char: 'C',
88 name: "Tent",
89 emoji: "\u{26FA}",
90 },
91 GlyphInfo {
92 index: 13,
93 hex_char: 'D',
94 name: "Rocket",
95 emoji: "\u{1F680}",
96 },
97 GlyphInfo {
98 index: 14,
99 hex_char: 'E',
100 name: "Tree",
101 emoji: "\u{1F333}",
102 },
103 GlyphInfo {
104 index: 15,
105 hex_char: 'F',
106 name: "Atlas",
107 emoji: "\u{1F53A}",
108 },
109];
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
113pub struct Glyph(u8);
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117#[non_exhaustive]
118pub struct GlyphParseError(pub String);
119
120impl fmt::Display for GlyphParseError {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(f, "invalid glyph: {}", self.0)
123 }
124}
125
126impl std::error::Error for GlyphParseError {}
127
128impl Glyph {
129 pub fn new(index: u8) -> Self {
131 assert!(index < 16, "glyph index must be 0-15, got {index}");
132 Self(index)
133 }
134
135 pub fn try_new(index: u8) -> Option<Self> {
137 if index < 16 { Some(Self(index)) } else { None }
138 }
139
140 pub fn index(self) -> u8 {
142 self.0
143 }
144
145 pub fn hex_char(self) -> char {
147 GLYPH_TABLE[self.0 as usize].hex_char
148 }
149
150 pub fn name(self) -> &'static str {
152 GLYPH_TABLE[self.0 as usize].name
153 }
154
155 pub fn emoji(self) -> &'static str {
157 GLYPH_TABLE[self.0 as usize].emoji
158 }
159}
160
161impl From<u8> for Glyph {
162 fn from(v: u8) -> Self {
164 Self::new(v)
165 }
166}
167
168impl From<Glyph> for u8 {
169 fn from(g: Glyph) -> u8 {
170 g.0
171 }
172}
173
174impl fmt::Display for Glyph {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 write!(f, "{}", self.hex_char())
178 }
179}
180
181impl FromStr for Glyph {
183 type Err = GlyphParseError;
184
185 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 if s.is_empty() {
187 return Err(GlyphParseError(s.to_string()));
188 }
189
190 if s.len() == 1 {
192 if let Some(v) = s.chars().next().and_then(|c| c.to_digit(16)) {
193 return Ok(Self(v as u8));
194 }
195 }
196
197 let stripped = s.trim_end_matches('\u{FE0F}');
199 for info in &GLYPH_TABLE {
200 let table_stripped = info.emoji.trim_end_matches('\u{FE0F}');
201 if stripped == table_stripped || s == info.emoji {
202 return Ok(Self(info.index));
203 }
204 }
205
206 let lower = s.to_lowercase();
208 for info in &GLYPH_TABLE {
209 if info.name.to_lowercase() == lower {
210 return Ok(Self(info.index));
211 }
212 }
213
214 Err(GlyphParseError(s.to_string()))
215 }
216}
217
218pub fn parse_next_glyph(input: &str) -> Result<(Glyph, &str), GlyphParseError> {
226 if input.is_empty() {
227 return Err(GlyphParseError("empty input".to_string()));
228 }
229
230 let mut by_len: Vec<(usize, &GlyphInfo)> =
233 GLYPH_TABLE.iter().map(|g| (g.emoji.len(), g)).collect();
234 by_len.sort_by(|a, b| b.0.cmp(&a.0));
235
236 for (_, info) in &by_len {
237 if let Some(rest) = input.strip_prefix(info.emoji) {
238 return Ok((Glyph(info.index), rest));
239 }
240 let emoji_no_vs = info.emoji.trim_end_matches('\u{FE0F}');
242 if emoji_no_vs != info.emoji {
243 if let Some(rest) = input.strip_prefix(emoji_no_vs) {
244 if rest.starts_with('\u{FE0F}') {
245 let vs_len = '\u{FE0F}'.len_utf8();
247 return Ok((Glyph(info.index), &rest[vs_len..]));
248 }
249 return Ok((Glyph(info.index), rest));
250 }
251 }
252 }
253
254 let mut names: Vec<&GlyphInfo> = GLYPH_TABLE.iter().collect();
256 names.sort_by(|a, b| b.name.len().cmp(&a.name.len()));
257
258 let input_lower = input.to_lowercase();
259 for info in &names {
260 let name_lower = info.name.to_lowercase();
261 if input_lower.starts_with(&name_lower) {
262 return Ok((Glyph(info.index), &input[info.name.len()..]));
263 }
264 }
265
266 let c = input.chars().next().unwrap();
268 if let Some(v) = c.to_digit(16) {
269 return Ok((Glyph(v as u8), &input[c.len_utf8()..]));
270 }
271
272 Err(GlyphParseError(format!(
273 "unrecognized glyph at: {}",
274 &input[..input.len().min(20)]
275 )))
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn all_16_glyphs_index_roundtrip() {
284 for i in 0..16u8 {
285 let g = Glyph::new(i);
286 assert_eq!(g.index(), i);
287 }
288 }
289
290 #[test]
291 fn all_16_glyphs_hex_roundtrip() {
292 let hex_chars = "0123456789ABCDEF";
293 for (i, c) in hex_chars.chars().enumerate() {
294 let g = Glyph::new(i as u8);
295 assert_eq!(g.hex_char(), c);
296 let parsed: Glyph = c.to_string().parse().unwrap();
297 assert_eq!(parsed.index(), i as u8);
298 }
299 }
300
301 #[test]
302 fn all_16_glyphs_emoji_roundtrip() {
303 for i in 0..16u8 {
304 let g = Glyph::new(i);
305 let emoji = g.emoji();
306 let parsed: Glyph = emoji.parse().unwrap();
307 assert_eq!(parsed.index(), i);
308 }
309 }
310
311 #[test]
312 fn all_16_glyphs_name_roundtrip() {
313 for i in 0..16u8 {
314 let g = Glyph::new(i);
315 let name = g.name();
316 let parsed: Glyph = name.parse().unwrap();
317 assert_eq!(parsed.index(), i);
318 }
319 }
320
321 #[test]
322 fn name_case_insensitive() {
323 assert_eq!("sunset".parse::<Glyph>().unwrap().index(), 0);
324 assert_eq!("SUNSET".parse::<Glyph>().unwrap().index(), 0);
325 assert_eq!("Sunset".parse::<Glyph>().unwrap().index(), 0);
326 }
327
328 #[test]
329 fn bird_with_and_without_variation_selector() {
330 let with_vs: Glyph = "\u{1F54A}\u{FE0F}".parse().unwrap();
331 assert_eq!(with_vs.index(), 1);
332
333 let without_vs: Glyph = "\u{1F54A}".parse().unwrap();
334 assert_eq!(without_vs.index(), 1);
335 }
336
337 #[test]
338 fn hex_lowercase() {
339 assert_eq!("a".parse::<Glyph>().unwrap().index(), 10);
340 assert_eq!("f".parse::<Glyph>().unwrap().index(), 15);
341 }
342
343 #[test]
344 fn invalid_glyph_errors() {
345 assert!("X".parse::<Glyph>().is_err());
346 assert!("hello".parse::<Glyph>().is_err());
347 assert!("".parse::<Glyph>().is_err());
348 }
349
350 #[test]
351 fn parse_next_glyph_emoji_sequence() {
352 let input = "\u{1F305}\u{1F54A}\u{FE0F}\u{1F41C}"; let (g1, rest) = parse_next_glyph(input).unwrap();
354 assert_eq!(g1.index(), 0); let (g2, rest) = parse_next_glyph(rest).unwrap();
356 assert_eq!(g2.index(), 1); let (g3, rest) = parse_next_glyph(rest).unwrap();
358 assert_eq!(g3.index(), 7); assert!(rest.is_empty());
360 }
361
362 #[test]
363 fn parse_next_glyph_hex() {
364 let (g, rest) = parse_next_glyph("A5").unwrap();
365 assert_eq!(g.index(), 10); let (g2, rest) = parse_next_glyph(rest).unwrap();
367 assert_eq!(g2.index(), 5); assert!(rest.is_empty());
369 }
370
371 #[test]
372 fn parse_next_glyph_name() {
373 let (g, rest) = parse_next_glyph("DragonflySunset").unwrap();
374 assert_eq!(g.index(), 8); let (g2, rest) = parse_next_glyph(rest).unwrap();
376 assert_eq!(g2.index(), 0); assert!(rest.is_empty());
378 }
379}