Skip to main content

panache_parser/parser/inlines/
emoji.rs

1use crate::syntax::SyntaxKind;
2use rowan::GreenNodeBuilder;
3
4/// Try to parse a textual emoji alias like `:smile:`.
5///
6/// Returns `(total_len, alias)` where alias excludes the surrounding colons.
7pub(crate) fn try_parse_emoji(text: &str) -> Option<(usize, &str)> {
8    let bytes = text.as_bytes();
9    if bytes.len() < 3 || bytes[0] != b':' {
10        return None;
11    }
12
13    let mut end = 1;
14    while end < bytes.len() {
15        let ch = bytes[end] as char;
16        if ch == ':' {
17            break;
18        }
19        if !ch.is_ascii_alphanumeric() && ch != '_' && ch != '+' && ch != '-' {
20            return None;
21        }
22        end += 1;
23    }
24
25    if end >= bytes.len() || bytes[end] != b':' || end == 1 {
26        return None;
27    }
28
29    // Avoid matching as emoji when immediately followed by word characters.
30    if end + 1 < bytes.len() {
31        let next = bytes[end + 1] as char;
32        if next.is_ascii_alphanumeric() || next == '_' {
33            return None;
34        }
35    }
36
37    Some((end + 1, &text[1..end]))
38}
39
40pub(crate) fn emit_emoji(builder: &mut GreenNodeBuilder, raw: &str) {
41    builder.start_node(SyntaxKind::EMOJI.into());
42    builder.token(SyntaxKind::TEXT.into(), raw);
43    builder.finish_node();
44}
45
46#[cfg(test)]
47mod tests {
48    use super::try_parse_emoji;
49
50    #[test]
51    fn parses_simple_alias() {
52        let parsed = try_parse_emoji(":smile:");
53        assert_eq!(parsed, Some((7, "smile")));
54    }
55
56    #[test]
57    fn parses_plus_one_alias() {
58        let parsed = try_parse_emoji(":+1:");
59        assert_eq!(parsed, Some((4, "+1")));
60    }
61
62    #[test]
63    fn rejects_spaces_inside_alias() {
64        assert!(try_parse_emoji(":not valid:").is_none());
65    }
66}