1use super::{ByteSet, Finder};
2use std::ops::Range;
3use std::sync::OnceLock;
4
5const VS15_BYTES: [u8; 3] = [0xEF, 0xB8, 0x8E];
7const VS16_BYTES: [u8; 3] = [0xEF, 0xB8, 0x8F];
9const KEYCAP_BYTES: [u8; 3] = [0xE2, 0x83, 0xA3];
11
12const EMOJI_PRESENTATION: &[(u32, u32)] = &[
19 (0x231A, 0x231B),
20 (0x23E9, 0x23EC),
21 (0x23F0, 0x23F0),
22 (0x23F3, 0x23F3),
23 (0x25FD, 0x25FE),
24 (0x2614, 0x2615),
25 (0x2648, 0x2653),
26 (0x267F, 0x267F),
27 (0x2693, 0x2693),
28 (0x26A1, 0x26A1),
29 (0x26AA, 0x26AB),
30 (0x26BD, 0x26BE),
31 (0x26C4, 0x26C5),
32 (0x26CE, 0x26CE),
33 (0x26D4, 0x26D4),
34 (0x26EA, 0x26EA),
35 (0x26F2, 0x26F3),
36 (0x26F5, 0x26F5),
37 (0x26FA, 0x26FA),
38 (0x26FD, 0x26FD),
39 (0x2705, 0x2705),
40 (0x270A, 0x270B),
41 (0x2728, 0x2728),
42 (0x274C, 0x274C),
43 (0x274E, 0x274E),
44 (0x2753, 0x2755),
45 (0x2757, 0x2757),
46 (0x2795, 0x2797),
47 (0x27B0, 0x27B0),
48 (0x27BF, 0x27BF),
49 (0x2B1B, 0x2B1C),
50 (0x2B50, 0x2B50),
51 (0x2B55, 0x2B55),
52 (0x1F004, 0x1F004),
53 (0x1F0CF, 0x1F0CF),
54 (0x1F170, 0x1F171),
55 (0x1F17E, 0x1F17F),
56 (0x1F18E, 0x1F18E),
57 (0x1F191, 0x1F19A),
58 (0x1F1E6, 0x1F1FF),
59 (0x1F201, 0x1F202),
60 (0x1F21A, 0x1F21A),
61 (0x1F22F, 0x1F22F),
62 (0x1F232, 0x1F23A),
63 (0x1F250, 0x1F251),
64 (0x1F300, 0x1F321),
65 (0x1F324, 0x1F393),
66 (0x1F396, 0x1F397),
67 (0x1F399, 0x1F39B),
68 (0x1F39E, 0x1F3F0),
69 (0x1F3F3, 0x1F3F5),
70 (0x1F3F7, 0x1F4FD),
71 (0x1F4FF, 0x1F53D),
72 (0x1F549, 0x1F54E),
73 (0x1F550, 0x1F567),
74 (0x1F56F, 0x1F570),
75 (0x1F573, 0x1F57A),
76 (0x1F587, 0x1F587),
77 (0x1F58A, 0x1F58D),
78 (0x1F590, 0x1F590),
79 (0x1F595, 0x1F596),
80 (0x1F5A4, 0x1F5A5),
81 (0x1F5A8, 0x1F5A8),
82 (0x1F5B1, 0x1F5B2),
83 (0x1F5BC, 0x1F5BC),
84 (0x1F5C2, 0x1F5C4),
85 (0x1F5D1, 0x1F5D3),
86 (0x1F5DC, 0x1F5DE),
87 (0x1F5E1, 0x1F5E1),
88 (0x1F5E3, 0x1F5E3),
89 (0x1F5E8, 0x1F5E8),
90 (0x1F5EF, 0x1F5EF),
91 (0x1F5F3, 0x1F5F3),
92 (0x1F5FA, 0x1F64F),
93 (0x1F680, 0x1F6C5),
94 (0x1F6CB, 0x1F6D2),
95 (0x1F6D5, 0x1F6D7),
96 (0x1F6DC, 0x1F6E5),
97 (0x1F6E9, 0x1F6E9),
98 (0x1F6EB, 0x1F6EC),
99 (0x1F6F0, 0x1F6F0),
100 (0x1F6F3, 0x1F6FC),
101 (0x1F7E0, 0x1F7EB),
102 (0x1F7F0, 0x1F7F0),
103 (0x1F90C, 0x1F93A),
104 (0x1F93C, 0x1F945),
105 (0x1F947, 0x1F9FF),
106 (0x1FA70, 0x1FA7C),
107 (0x1FA80, 0x1FA88),
108 (0x1FA90, 0x1FABD),
109 (0x1FABF, 0x1FAC5),
110 (0x1FACE, 0x1FADB),
111 (0x1FAE0, 0x1FAE8),
112 (0x1FAF0, 0x1FAF8),
113];
114
115const TEXT_DEFAULT_EMOJI: &[(u32, u32)] = &[
121 (0x00A9, 0x00A9),
122 (0x00AE, 0x00AE),
123 (0x203C, 0x203C),
124 (0x2049, 0x2049),
125 (0x2122, 0x2122),
126 (0x2139, 0x2139),
127 (0x2194, 0x2199),
128 (0x21A9, 0x21AA),
129 (0x2328, 0x2328),
130 (0x23CF, 0x23CF),
131 (0x23ED, 0x23EF),
132 (0x23F1, 0x23F2),
133 (0x23F8, 0x23FA),
134 (0x24C2, 0x24C2),
135 (0x25AA, 0x25AB),
136 (0x25B6, 0x25B6),
137 (0x25C0, 0x25C0),
138 (0x25FB, 0x25FC),
139 (0x2600, 0x2604),
140 (0x260E, 0x260E),
141 (0x2611, 0x2611),
142 (0x2618, 0x2618),
143 (0x261D, 0x261D),
144 (0x2620, 0x2620),
145 (0x2622, 0x2623),
146 (0x2626, 0x2626),
147 (0x262A, 0x262A),
148 (0x262E, 0x262F),
149 (0x2638, 0x263A),
150 (0x2640, 0x2640),
151 (0x2642, 0x2642),
152 (0x265F, 0x2660),
153 (0x2663, 0x2663),
154 (0x2665, 0x2666),
155 (0x2668, 0x2668),
156 (0x267B, 0x267B),
157 (0x267E, 0x267E),
158 (0x2692, 0x2692),
159 (0x2694, 0x2697),
160 (0x2699, 0x2699),
161 (0x269B, 0x269C),
162 (0x26A0, 0x26A0),
163 (0x26A7, 0x26A7),
164 (0x26B0, 0x26B1),
165 (0x26C8, 0x26C8),
166 (0x26CF, 0x26CF),
167 (0x26D1, 0x26D1),
168 (0x26D3, 0x26D3),
169 (0x26E9, 0x26E9),
170 (0x26F0, 0x26F1),
171 (0x26F4, 0x26F4),
172 (0x26F7, 0x26F9),
173 (0x2702, 0x2702),
174 (0x2708, 0x2709),
175 (0x270C, 0x270D),
176 (0x270F, 0x270F),
177 (0x2712, 0x2712),
178 (0x2714, 0x2714),
179 (0x2716, 0x2716),
180 (0x271D, 0x271D),
181 (0x2721, 0x2721),
182 (0x2733, 0x2734),
183 (0x2744, 0x2744),
184 (0x2747, 0x2747),
185 (0x2763, 0x2764),
186 (0x27A1, 0x27A1),
187 (0x2934, 0x2935),
188 (0x2B05, 0x2B07),
189 (0x3030, 0x3030),
190 (0x303D, 0x303D),
191 (0x3297, 0x3297),
192 (0x3299, 0x3299),
193];
194
195#[derive(Default)]
196pub struct Emoji {}
197
198impl Emoji {
199 fn in_table(table: &[(u32, u32)], cp: u32) -> bool {
200 table
201 .binary_search_by(|&(lo, hi)| {
202 if hi < cp {
203 std::cmp::Ordering::Less
204 } else if lo > cp {
205 std::cmp::Ordering::Greater
206 } else {
207 std::cmp::Ordering::Equal
208 }
209 })
210 .is_ok()
211 }
212
213 fn is_emoji_presentation(c: char) -> bool {
215 Self::in_table(EMOJI_PRESENTATION, c as u32)
216 }
217
218 fn is_text_default_emoji(c: char) -> bool {
220 Self::in_table(TEXT_DEFAULT_EMOJI, c as u32)
221 }
222
223 fn is_pictographic(c: char) -> bool {
225 Self::is_emoji_presentation(c) || Self::is_text_default_emoji(c)
226 }
227
228 fn is_keycap_base(byte: u8) -> bool {
230 matches!(byte, b'0'..=b'9' | b'#' | b'*')
231 }
232
233 fn is_regional_indicator(c: char) -> bool {
234 (0x1F1E6..=0x1F1FF).contains(&(c as u32))
235 }
236
237 fn is_skin_tone(c: char) -> bool {
238 (0x1F3FB..=0x1F3FF).contains(&(c as u32))
239 }
240
241 fn is_tag_char(c: char) -> bool {
242 (0xE0020..=0xE007E).contains(&(c as u32))
243 }
244
245 fn is_tag_cancel(c: char) -> bool {
246 c == '\u{E007F}'
247 }
248
249 fn decode_char(input: &[u8], pos: usize) -> Option<(char, usize)> {
255 let lead = *input.get(pos)?;
256 let len = match lead {
257 0x00..=0x7F => 1,
258 0xC2..=0xDF => 2,
259 0xE0..=0xEF => 3,
260 0xF0..=0xF4 => 4,
261 _ => return None,
262 };
263 let bytes = input.get(pos..pos.checked_add(len)?)?;
264 let c = std::str::from_utf8(bytes).ok()?.chars().next()?;
265 Some((c, len))
266 }
267
268 fn bytes_at(input: &[u8], pos: usize, pat: &[u8; 3]) -> bool {
270 pos.checked_add(3)
271 .and_then(|end| input.get(pos..end))
272 .is_some_and(|window| window == pat)
273 }
274
275 fn keycap_len(input: &[u8], pos: usize) -> Option<usize> {
278 if Self::bytes_at(input, pos + 1, &KEYCAP_BYTES) {
279 return Some(1 + KEYCAP_BYTES.len());
280 }
281 if Self::bytes_at(input, pos + 1, &VS16_BYTES)
282 && Self::bytes_at(input, pos + 1 + VS16_BYTES.len(), &KEYCAP_BYTES)
283 {
284 return Some(1 + VS16_BYTES.len() + KEYCAP_BYTES.len());
285 }
286 None
287 }
288
289 fn prefix_ok(lead: u8, second: u8) -> bool {
300 static TABLE: OnceLock<[ByteSet; 256]> = OnceLock::new();
301 let table = TABLE.get_or_init(|| {
302 let mut table = [ByteSet::EMPTY; 256];
303 for &(lo, hi) in EMOJI_PRESENTATION.iter().chain(TEXT_DEFAULT_EMOJI) {
304 let mut cp = lo;
307 while cp <= hi {
308 if let Some(c) = char::from_u32(cp) {
309 let mut buf = [0u8; 4];
310 let bytes = c.encode_utf8(&mut buf).as_bytes();
311 if bytes.len() >= 2 {
312 table[bytes[0] as usize] = table[bytes[0] as usize].with(bytes[1]);
313 }
314 }
315 cp = if cp < 0x800 { cp + 1 } else { (cp | 63) + 1 };
318 }
319 }
320 table
321 });
322 table[lead as usize].contains(second)
323 }
324
325 fn match_len_at(input: &[u8], pos: usize) -> Option<usize> {
326 let lead = *input.get(pos)?;
327
328 if lead.is_ascii() {
329 if !Self::is_keycap_base(lead) {
330 return None;
331 }
332 return Self::keycap_len(input, pos);
333 }
334 if !input
335 .get(pos + 1)
336 .is_some_and(|&second| Self::prefix_ok(lead, second))
337 {
338 return None;
339 }
340
341 let (first, first_len) = Self::decode_char(input, pos)?;
342 let after = pos + first_len;
343
344 if Self::is_emoji_presentation(first) {
345 if Self::bytes_at(input, after, &VS15_BYTES) {
348 return None;
349 }
350 return Some(Self::consume_sequence(input, pos, first, first_len));
351 }
352
353 if Self::is_text_default_emoji(first) && Self::bytes_at(input, after, &VS16_BYTES) {
354 return Some(Self::consume_sequence(input, pos, first, first_len));
355 }
356
357 None
358 }
359
360 fn consume_sequence(input: &[u8], pos: usize, first: char, first_len: usize) -> usize {
363 let mut end = pos + first_len;
364
365 if Self::is_regional_indicator(first) {
366 if let Some((c, len)) = Self::decode_char(input, end)
367 && Self::is_regional_indicator(c)
368 {
369 end += len;
370 }
371 return end - pos;
372 }
373
374 if first == '\u{1F3F4}' {
375 let mut cursor = end;
376 let mut has_tags = false;
377 loop {
378 match Self::decode_char(input, cursor) {
379 Some((c, len)) if Self::is_tag_char(c) => {
380 has_tags = true;
381 cursor += len;
382 }
383 Some((c, len)) if Self::is_tag_cancel(c) => {
384 has_tags = true;
385 cursor += len;
386 break;
387 }
388 _ => break,
389 }
390 }
391 if has_tags {
392 return cursor - pos;
393 }
394 }
395
396 loop {
397 if let Some((c, len)) = Self::decode_char(input, end)
398 && c == '\u{FE0F}'
399 {
400 end += len;
401 }
402
403 if let Some((c, len)) = Self::decode_char(input, end)
404 && Self::is_skin_tone(c)
405 {
406 end += len;
407 }
408
409 if let Some((c, zwj_len)) = Self::decode_char(input, end)
410 && c == '\u{200D}'
411 && let Some((next, next_len)) = Self::decode_char(input, end + zwj_len)
412 && Self::is_pictographic(next)
413 {
414 end += zwj_len + next_len;
415 continue;
416 }
417
418 break;
419 }
420
421 end - pos
422 }
423}
424
425impl Finder for Emoji {
426 fn line_agnostic(&self) -> bool {
427 true
430 }
431
432 fn id(&self) -> &'static str {
433 "emoji"
434 }
435
436 fn dispatchable(&self) -> bool {
437 true
438 }
439
440 fn could_start_at(&self, byte: u8) -> bool {
441 matches!(byte, 0xC2 | 0xE2 | 0xE3 | 0xF0) || Self::is_keycap_base(byte)
445 }
446
447 fn could_continue_with(&self, cur: u8, next: u8) -> bool {
448 if Self::is_keycap_base(cur) {
449 next == VS16_BYTES[0] || next == KEYCAP_BYTES[0]
450 } else {
451 next & 0xC0 == 0x80
453 }
454 }
455
456 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
457 let len = Self::match_len_at(input, pos)?;
458 Some(pos..pos + len)
459 }
460
461 fn find(&self, s: &str) -> Option<Range<usize>> {
462 let input = s.as_bytes();
463 for (byte_pos, _) in s.char_indices() {
464 if let Some(len) = Self::match_len_at(input, byte_pos) {
465 return Some(byte_pos..byte_pos + len);
466 }
467 }
468 None
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn id_should_return_emoji() {
478 let finder = Emoji::default();
479 assert_eq!("emoji", finder.id());
480 }
481
482 #[test]
483 fn tables_are_sorted_and_disjoint() {
484 for table in [EMOJI_PRESENTATION, TEXT_DEFAULT_EMOJI] {
485 let mut prev_hi: Option<u32> = None;
486 for &(lo, hi) in table {
487 assert!(lo <= hi, "range {lo:#X}..={hi:#X} is reversed");
488 if let Some(p) = prev_hi {
489 assert!(p < lo, "range starting at {lo:#X} overlaps or is unsorted");
490 }
491 prev_hi = Some(hi);
492 }
493 }
494 for &(lo, hi) in TEXT_DEFAULT_EMOJI {
496 for cp in lo..=hi {
497 assert!(
498 !Emoji::in_table(EMOJI_PRESENTATION, cp),
499 "{cp:#X} is in both tiers"
500 );
501 }
502 }
503 }
504
505 #[test]
506 fn in_table_hits_boundaries_and_rejects_gaps() {
507 for cp in [0x231A, 0x231B, 0x2B55, 0x1F004, 0x1F600, 0x1FAF8] {
508 assert!(Emoji::in_table(EMOJI_PRESENTATION, cp), "{cp:#X}");
509 }
510 for cp in [0x2605, 0x2713, 0x2717, 0x1F000, 0x1F031, 0x1FAF9, 0x0041] {
511 assert!(!Emoji::in_table(EMOJI_PRESENTATION, cp), "{cp:#X}");
512 assert!(!Emoji::in_table(TEXT_DEFAULT_EMOJI, cp), "{cp:#X}");
513 }
514 for cp in [0x00A9, 0x00AE, 0x2122, 0x2600, 0x2764, 0x3299] {
515 assert!(Emoji::in_table(TEXT_DEFAULT_EMOJI, cp), "{cp:#X}");
516 }
517 }
518
519 #[test]
520 fn find_should_extract_simple_emoji() {
521 let finder = Emoji::default();
522 let input = "hello 😀 world";
523 let range = finder.find(input).unwrap();
524 assert_eq!("😀", &input[range]);
525 }
526
527 #[test]
528 fn find_should_extract_emoji_at_start() {
529 let finder = Emoji::default();
530 let input = "🎉 party";
531 let range = finder.find(input).unwrap();
532 assert_eq!("🎉", &input[range]);
533 }
534
535 #[test]
536 fn find_should_extract_emoji_at_end() {
537 let finder = Emoji::default();
538 let input = "done ✅";
539 let range = finder.find(input).unwrap();
540 assert_eq!("✅", &input[range]);
541 }
542
543 #[test]
544 fn find_should_extract_emoji_with_skin_tone() {
545 let finder = Emoji::default();
546 let input = "wave 👋🏽 here";
547 let range = finder.find(input).unwrap();
548 assert_eq!("👋🏽", &input[range]);
549 }
550
551 #[test]
552 fn find_should_extract_zwj_sequence() {
553 let finder = Emoji::default();
554 let input = "family 👨\u{200D}👩\u{200D}👧 end";
555 let range = finder.find(input).unwrap();
556 assert_eq!("👨\u{200D}👩\u{200D}👧", &input[range]);
557 }
558
559 #[test]
560 fn find_should_extract_flag() {
561 let finder = Emoji::default();
562 let input = "flag 🇺🇸 end";
563 let range = finder.find(input).unwrap();
564 assert_eq!("🇺🇸", &input[range]);
565 }
566
567 #[test]
568 fn find_should_extract_emoji_with_variation_selector() {
569 let finder = Emoji::default();
570 let input = "heart ❤\u{FE0F} end";
571 let range = finder.find(input).unwrap();
572 assert_eq!("❤\u{FE0F}", &input[range]);
573 }
574
575 #[test]
576 fn find_should_extract_tag_sequence() {
577 let finder = Emoji::default();
578 let input = "flag 🏴\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F} end";
579 let range = finder.find(input).unwrap();
580 assert_eq!(
581 "🏴\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}",
582 &input[range]
583 );
584 }
585
586 #[test]
587 fn find_should_extract_zwj_with_skin_tone() {
588 let finder = Emoji::default();
589 let input = "👩🏽\u{200D}🔬 scientist";
590 let range = finder.find(input).unwrap();
591 assert_eq!("👩🏽\u{200D}🔬", &input[range]);
592 }
593
594 #[test]
595 fn find_should_extract_zwj_with_variation_selector() {
596 let finder = Emoji::default();
597 let input = "❤\u{FE0F}\u{200D}🔥";
598 let range = finder.find(input).unwrap();
599 assert_eq!("❤\u{FE0F}\u{200D}🔥", &input[range]);
600 }
601
602 #[test]
603 fn find_should_extract_multiple_emojis_iteratively() {
604 let finder = Emoji::default();
605 let input = "😀🎉✅";
606
607 let mut results = Vec::new();
608 let mut idx = 0;
609 while idx < input.len() {
610 if let Some(range) = finder.find(&input[idx..]) {
611 results.push(&input[idx + range.start..idx + range.end]);
612 idx += range.end;
613 } else {
614 break;
615 }
616 }
617
618 assert_eq!(vec!["😀", "🎉", "✅"], results);
619 }
620
621 #[test]
622 fn find_should_extract_adjacent_flags() {
623 let finder = Emoji::default();
624 let input = "🇺🇸🇬🇧";
625
626 let mut results = Vec::new();
627 let mut idx = 0;
628 while idx < input.len() {
629 if let Some(range) = finder.find(&input[idx..]) {
630 results.push(&input[idx + range.start..idx + range.end]);
631 idx += range.end;
632 } else {
633 break;
634 }
635 }
636
637 assert_eq!(vec!["🇺🇸", "🇬🇧"], results);
638 }
639
640 #[test]
641 fn find_should_handle_empty_input() {
642 let finder = Emoji::default();
643 assert!(finder.find("").is_none());
644 }
645
646 #[test]
647 fn find_should_reject_plain_ascii() {
648 let finder = Emoji::default();
649 assert!(finder.find("hello world 123").is_none());
650 }
651
652 #[test]
653 fn find_should_not_start_with_zwj() {
654 let finder = Emoji::default();
655 assert!(finder.find("\u{200D}hello").is_none());
656 }
657
658 #[test]
659 fn find_should_extract_misc_symbol_with_vs16_only() {
660 let finder = Emoji::default();
663 assert!(finder.find("sun ☀ here").is_none());
664 let input = "sun ☀\u{FE0F} here";
665 let range = finder.find(input).unwrap();
666 assert_eq!("☀\u{FE0F}", &input[range]);
667 }
668
669 #[test]
670 fn find_should_extract_dingbat_with_vs16_only() {
671 let finder = Emoji::default();
673 assert!(finder.find("check ✂ here").is_none());
674 let input = "check ✂\u{FE0F} here";
675 let range = finder.find(input).unwrap();
676 assert_eq!("✂\u{FE0F}", &input[range]);
677 }
678
679 #[test]
680 fn find_should_extract_supplemental_arrow_with_vs16_only() {
681 let finder = Emoji::default();
683 assert!(finder.find("go ➡ there").is_none());
684 let input = "go ➡\u{FE0F} there";
685 let range = finder.find(input).unwrap();
686 assert_eq!("➡\u{FE0F}", &input[range]);
687 }
688
689 #[test]
690 fn find_should_extract_emoji_in_brackets() {
691 let finder = Emoji::default();
692 let input = "[😀]";
693 let range = finder.find(input).unwrap();
694 assert_eq!("😀", &input[range]);
695 }
696
697 #[test]
700 fn try_at_simple_emoji() {
701 let finder = Emoji::default();
702 let input = "😀 rest".as_bytes();
703 let range = finder.try_at(input, 0).unwrap();
704 assert_eq!(range, 0..4);
705 }
706
707 #[test]
708 fn try_at_emoji_after_ascii() {
709 let finder = Emoji::default();
710 let input = "hi 😀 rest";
711 let bytes = input.as_bytes();
712 let range = finder.try_at(bytes, 3).unwrap();
713 assert_eq!(&input[range], "😀");
714 }
715
716 #[test]
717 fn try_at_emoji_with_skin_tone() {
718 let finder = Emoji::default();
719 let input = "👋🏽";
720 let bytes = input.as_bytes();
721 let range = finder.try_at(bytes, 0).unwrap();
722 assert_eq!(&input[range], "👋🏽");
723 }
724
725 #[test]
726 fn try_at_non_emoji_multibyte() {
727 let finder = Emoji::default();
728 let input = "héllo";
729 let bytes = input.as_bytes();
730 assert!(finder.try_at(bytes, 0).is_none());
731 }
732
733 #[test]
734 fn try_at_zwj_sequence_via_dispatch() {
735 let finder = Emoji::default();
736 let input = "👨\u{200D}👩\u{200D}👧";
737 let bytes = input.as_bytes();
738 let range = finder.try_at(bytes, 0).unwrap();
739 assert_eq!(&input[range], "👨\u{200D}👩\u{200D}👧");
740 }
741
742 #[test]
743 fn try_at_flag_sequence() {
744 let finder = Emoji::default();
745 let input = "🇺🇸";
746 let bytes = input.as_bytes();
747 let range = finder.try_at(bytes, 0).unwrap();
748 assert_eq!(&input[range], "🇺🇸");
749 }
750
751 #[test]
752 fn try_at_mid_zwj_matches_and_scanner_suppresses() {
753 let finder = Emoji::default();
758 let input = "👨\u{200D}👩";
759 let bytes = input.as_bytes();
760 let emoji2_start = "👨\u{200D}".len();
761 assert_eq!(
762 finder.try_at(bytes, emoji2_start),
763 Some(emoji2_start..input.len())
764 );
765 }
766}