1use std::collections::HashMap;
34use std::fs;
35use std::path::Path;
36
37static BUNDLED_EMOJI_DATA: &str =
40 include_str!("../data/emoji-data.txt");
41
42pub fn bundled_emoji_sequences() -> HashMap<String, String> {
57 parse_emoji_sequences(BUNDLED_EMOJI_DATA)
58}
59
60pub fn parse_emoji_sequences(
86 contents: &str,
87) -> HashMap<String, String> {
88 let mut map = HashMap::new();
89
90 for raw_line in contents.lines() {
91 let line = raw_line.trim();
92
93 if line.is_empty() || line.starts_with('#') {
95 continue;
96 }
97
98 let (data_part, comment_part) = match line.split_once('#') {
100 Some((before, after)) => (before.trim(), after.trim()),
101 None => (line, ""),
102 };
103
104 let raw_label_after_paren =
106 if let Some(close_paren_idx) = comment_part.find(')') {
107 &comment_part[close_paren_idx + 1..]
108 } else {
109 comment_part
110 };
111
112 let short_label = raw_label_after_paren
114 .trim()
115 .to_lowercase()
116 .split_whitespace()
117 .collect::<Vec<_>>()
118 .join("-");
119
120 let data_fields: Vec<&str> =
122 data_part.split(';').map(|s| s.trim()).collect();
123 if data_fields.is_empty() {
124 continue;
125 }
126
127 let hex_seq = data_fields[0];
129
130 let emoji_string: String = hex_seq
132 .split_whitespace()
133 .filter_map(|hex| u32::from_str_radix(hex, 16).ok())
134 .flat_map(char::from_u32)
135 .collect();
136
137 if emoji_string.is_empty() {
138 continue; }
140
141 let _ = map.insert(emoji_string, short_label);
143 }
144
145 map
146}
147
148pub fn load_emoji_sequences<P: AsRef<Path>>(
177 filepath: P,
178) -> Result<HashMap<String, String>, std::io::Error> {
179 let contents = fs::read_to_string(filepath)?;
180 Ok(parse_emoji_sequences(&contents))
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use std::io::Write;
187 use tempfile::NamedTempFile;
188
189 fn create_temp_file(content: &str) -> NamedTempFile {
191 let mut file = NamedTempFile::new()
192 .expect("Failed to create temporary file");
193 file.write_all(content.as_bytes())
194 .expect("Failed to write to temporary file");
195 file
196 }
197
198 #[cfg_attr(
199 miri,
200 ignore = "touches the filesystem; Miri isolation forbids it"
201 )]
202 #[test]
203 fn test_load_emoji_sequences_basic() {
204 let test_data = r#"
205 26A1 ; emoji ; L1 ; none ; a j # V4.0 (⚡) HIGH VOLTAGE SIGN
206 1F600 ; emoji ; L1 ; none ; j # V6.0 (😀) GRINNING FACE
207 "#;
208
209 let file = create_temp_file(test_data);
210
211 let result = load_emoji_sequences(file.path()).unwrap();
212
213 let mut expected = HashMap::new();
214 let _ = expected
215 .insert("⚡".to_string(), "high-voltage-sign".to_string());
216 let _ = expected
217 .insert("😀".to_string(), "grinning-face".to_string());
218
219 assert_eq!(result, expected);
220 }
221
222 #[cfg_attr(
223 miri,
224 ignore = "touches the filesystem; Miri isolation forbids it"
225 )]
226 #[test]
227 fn test_load_emoji_sequences_empty_file() {
228 let test_data = "";
229
230 let file = create_temp_file(test_data);
231
232 let result = load_emoji_sequences(file.path());
233
234 assert!(result.unwrap().is_empty());
235 }
236
237 #[cfg_attr(
238 miri,
239 ignore = "touches the filesystem; Miri isolation forbids it"
240 )]
241 #[test]
242 fn test_load_emoji_sequences_with_comments_and_blanks() {
243 let test_data = r#"
244 # This is a comment
245
246 1F44D ; emoji ; L1 ; none ; j # V6.0 (👍) THUMBS UP SIGN
247
248 # Another comment here
249
250"#;
251
252 let file = create_temp_file(test_data);
253
254 let result = load_emoji_sequences(file.path());
255
256 let mut expected = HashMap::new();
257 let _ = expected
258 .insert("👍".to_string(), "thumbs-up-sign".to_string());
259
260 assert_eq!(result.unwrap(), expected);
261 }
262
263 #[cfg_attr(
264 miri,
265 ignore = "touches the filesystem; Miri isolation forbids it"
266 )]
267 #[test]
268 fn test_load_emoji_sequences_no_comment_label() {
269 let test_data = r#"
270 1F4AF ; emoji ; L1 ; none ; j # V6.0 (💯) HUNDRED POINTS SYMBOL
271 1F602 ; emoji ; L1 ; none ; j
272"#;
273
274 let file = create_temp_file(test_data);
275
276 let result = load_emoji_sequences(file.path());
277
278 let mut expected = HashMap::new();
279 let _ = expected.insert(
280 "💯".to_string(),
281 "hundred-points-symbol".to_string(),
282 );
283 let _ = expected.insert("😂".to_string(), "".to_string()); assert_eq!(result.unwrap(), expected);
286 }
287
288 #[cfg_attr(
289 miri,
290 ignore = "touches the filesystem; Miri isolation forbids it"
291 )]
292 #[test]
293 fn test_load_emoji_sequences_invalid_hex_code() {
294 let test_data = r#"
295 26A1 ; emoji ; L1 ; none ; a j # V4.0 (⚡) HIGH VOLTAGE SIGN
296 INVALID_HEX ; emoji ; L1 ; none ; j # Invalid hex code
297"#;
298
299 let file = create_temp_file(test_data);
300
301 let result = load_emoji_sequences(file.path());
302
303 let mut expected = HashMap::new();
304 let _ = expected
305 .insert("⚡".to_string(), "high-voltage-sign".to_string());
306
307 assert_eq!(result.unwrap(), expected);
308 }
309
310 #[cfg_attr(
311 miri,
312 ignore = "touches the filesystem; Miri isolation forbids it"
313 )]
314 #[test]
315 fn test_load_emoji_sequences_multi_codepoint() {
316 let test_data = r#"
317 1F1E6 1F1FA ; emoji ; L1 ; none ; j # V6.0 (🇦🇺) FLAG FOR AUSTRALIA
318"#;
319
320 let file = create_temp_file(test_data);
321
322 let result = load_emoji_sequences(file.path());
323
324 let mut expected = HashMap::new();
325 let _ = expected
326 .insert("🇦🇺".to_string(), "flag-for-australia".to_string());
327
328 assert_eq!(result.unwrap(), expected);
329 }
330
331 #[cfg_attr(
332 miri,
333 ignore = "touches the filesystem; Miri isolation forbids it"
334 )]
335 #[test]
336 fn test_load_emoji_sequences_missing_label() {
337 let test_data = r#"
338 1F44D ; emoji ; L1 ; none ; j # V6.0 (👍) THUMBS UP SIGN
339 1F602 ; emoji ; L1 ; none ; j
340 1F600 ; emoji ; L1 ; none ; j #
341"#;
342
343 let file = create_temp_file(test_data);
344
345 let result = load_emoji_sequences(file.path());
346
347 let mut expected = HashMap::new();
348 let _ = expected
349 .insert("👍".to_string(), "thumbs-up-sign".to_string());
350 let _ = expected.insert("😂".to_string(), "".to_string()); let _ = expected.insert("😀".to_string(), "".to_string()); assert_eq!(result.unwrap(), expected);
354 }
355
356 #[cfg_attr(
357 miri,
358 ignore = "touches the filesystem; Miri isolation forbids it"
359 )]
360 #[test]
361 fn test_load_emoji_sequences_handles_empty_and_whitespace() {
362 let test_data = r#"
363
364 1F602 ; emoji ; L1 ; none ; j # V6.0 (😂) FACE WITH TEARS OF JOY
365
366 "#;
367
368 let file = create_temp_file(test_data);
369
370 let result = load_emoji_sequences(file.path());
371
372 let mut expected = HashMap::new();
373 let _ = expected.insert(
374 "😂".to_string(),
375 "face-with-tears-of-joy".to_string(),
376 );
377
378 assert_eq!(result.unwrap(), expected);
379 }
380
381 #[cfg_attr(
382 miri,
383 ignore = "touches the filesystem; Miri isolation forbids it"
384 )]
385 #[test]
386 fn test_load_emoji_sequences_handles_trailing_whitespace() {
387 let test_data = r#"
388 1F602 ; emoji ; L1 ; none ; j # V6.0 (😂) FACE WITH TEARS OF JOY
389 "#;
390
391 let file = create_temp_file(test_data);
392
393 let result = load_emoji_sequences(file.path());
394
395 let mut expected = HashMap::new();
396 let _ = expected.insert(
397 "😂".to_string(),
398 "face-with-tears-of-joy".to_string(),
399 );
400
401 assert_eq!(result.unwrap(), expected);
402 }
403
404 #[cfg_attr(
405 miri,
406 ignore = "touches the filesystem; Miri isolation forbids it"
407 )]
408 #[test]
409 fn test_load_emoji_sequences_skip_invalid_lines() {
410 let test_data = r#"
411 # Comment line
412 ; invalid line ; no hex code ; # Just semicolons
413 1F602 ; emoji ; L1 ; none ; j # V6.0 (😂) FACE WITH TEARS OF JOY
414 "#;
415
416 let file = create_temp_file(test_data);
417 let result = load_emoji_sequences(file.path()).unwrap();
418
419 let mut expected = HashMap::new();
421 let _ = expected.insert(
422 "😂".to_string(),
423 "face-with-tears-of-joy".to_string(),
424 );
425 assert_eq!(result, expected);
426 }
427
428 #[cfg_attr(
429 miri,
430 ignore = "touches the filesystem; Miri isolation forbids it"
431 )]
432 #[test]
433 fn test_load_emoji_sequences_split_behavior() {
434 let test_data = r#"
435 26A1;emoji;L1;none;a j# V4.0 (⚡) HIGH VOLTAGE SIGN
436 1F602 ; emoji ; L1 ; none ; j # V6.0 (😂) FACE WITH TEARS OF JOY
437 26A1 ; emoji ; L1 ; none ; a j # V4.0 (⚡) HIGH VOLTAGE SIGN
438 "#;
439
440 let file = create_temp_file(test_data);
441 let result = load_emoji_sequences(file.path()).unwrap();
442
443 let mut expected = HashMap::new();
444 let _ = expected
445 .insert("⚡".to_string(), "high-voltage-sign".to_string());
446 let _ = expected.insert(
447 "😂".to_string(),
448 "face-with-tears-of-joy".to_string(),
449 );
450 assert_eq!(result, expected);
451 }
452
453 #[cfg_attr(
454 miri,
455 ignore = "touches the filesystem; Miri isolation forbids it"
456 )]
457 #[test]
458 fn test_load_emoji_sequences_parenthesis_variations() {
459 let test_data = r#"
460 26A1 ; emoji ; L1 ; none ; a j # (⚡) HIGH VOLTAGE
461 1F602 ; emoji ; L1 ; none ; j # V6.0 (😂) FACE WITH TEARS
462 1F603 ; emoji ; L1 ; none ; j # V6.0 (😃) SMILEY FACE
463 1F604 ; emoji ; L1 ; none ; j # V6.0 (😄) GRINNING FACE
464 "#;
465
466 let file = create_temp_file(test_data);
467 let result = load_emoji_sequences(file.path()).unwrap();
468
469 let mut expected = HashMap::new();
470 let _ = expected
471 .insert("⚡".to_string(), "high-voltage".to_string());
472 let _ = expected
473 .insert("😂".to_string(), "face-with-tears".to_string());
474 let _ = expected
475 .insert("😃".to_string(), "smiley-face".to_string());
476 let _ = expected
477 .insert("😄".to_string(), "grinning-face".to_string());
478 assert_eq!(result, expected);
479 }
480
481 #[cfg_attr(
482 miri,
483 ignore = "touches the filesystem; Miri isolation forbids it"
484 )]
485 #[test]
486 fn test_load_emoji_sequences_unparseable_sequences() {
487 let test_data = r#"
488 110000 ; emoji ; L1 ; none ; j # Above Unicode range INVALID
489 1F602 ; emoji ; L1 ; none ; j # V6.0 (😂) FACE WITH TEARS OF JOY
490 D800 ; emoji ; L1 ; none ; j # Surrogate code point
491 "#;
492
493 let file = create_temp_file(test_data);
494 let result = load_emoji_sequences(file.path()).unwrap();
495
496 let mut expected = HashMap::new();
498 let _ = expected.insert(
499 "😂".to_string(),
500 "face-with-tears-of-joy".to_string(),
501 );
502 assert_eq!(result, expected);
503 }
504
505 #[cfg_attr(
506 miri,
507 ignore = "touches the filesystem; Miri isolation forbids it"
508 )]
509 #[test]
510 fn test_load_emoji_sequences_empty_fields() {
511 let test_data = r#"
512 ; ; ; ; ; # Empty fields should be skipped
513 1F602 ; emoji ; L1 ; none ; j # V6.0 (😂) FACE WITH TEARS OF JOY
514 #
515 "#;
516
517 let file = create_temp_file(test_data);
518 let result = load_emoji_sequences(file.path()).unwrap();
519
520 let mut expected = HashMap::new();
521 let _ = expected.insert(
522 "😂".to_string(),
523 "face-with-tears-of-joy".to_string(),
524 );
525 assert_eq!(result, expected);
526 }
527
528 #[cfg_attr(
529 miri,
530 ignore = "touches the filesystem; Miri isolation forbids it"
531 )]
532 #[test]
533 fn test_load_emoji_sequences_whitespace_variations() {
534 let test_data = r#"
535 1F602;emoji;L1;none;j# V6.0 (😂) FACE WITH TEARS OF JOY
536 1F603 ; emoji ; L1 ; none ; j # V6.0 (😃) SMILEY FACE
537 "#;
538
539 let file = create_temp_file(test_data);
540 let result = load_emoji_sequences(file.path()).unwrap();
541
542 let mut expected = HashMap::new();
543 let _ = expected.insert(
544 "😂".to_string(),
545 "face-with-tears-of-joy".to_string(),
546 );
547 let _ = expected
548 .insert("😃".to_string(), "smiley-face".to_string());
549 assert_eq!(result, expected);
550 }
551}