1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use std::ops::Range;

/// Emotes are little pictograms used inline in twitch messages
///
/// They are presented (to the irc connection) in a
/// `id1:range1,range2/id2:range1,..` form which marks the byte position that
/// the emote is at.
///
/// Examples:
///
/// `"testing Kappa"` would be `25:8-13`
/// `"Kappa testing Kappa"` would be `25:0-5,14-19`
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Emotes {
    /// The emote id, e.g. `Kappa = 25`
    pub id: usize,
    /// A list of [`Range`](https://doc.rust-lang.org/std/ops/struct.Range.html) in the message where this emote is
    /// found
    pub ranges: Vec<Range<u16>>,
}

impl Emotes {
    pub(in crate) fn parse<'a>(input: &'a str) -> impl Iterator<Item = Self> + 'a {
        input
            .split_terminator('/')
            .filter_map(|s| Self::get_parts(s, ':'))
            .filter_map(|(head, tail)| {
                Some(Self {
                    id: head.parse().ok()?,
                    ranges: Self::get_ranges(&tail).collect(),
                })
            })
    }

    #[inline]
    fn get_ranges<'a>(tail: &'a str) -> impl Iterator<Item = Range<u16>> + 'a {
        tail.split_terminator(',')
            .map(|s| Self::get_parts(s, '-'))
            .filter_map(move |parts| {
                let (start, end) = parts?;
                let (start, end) = (start.parse().ok()?, end.parse().ok()?);
                Some(Range { start, end })
            })
    }

    #[inline(always)]
    fn get_parts(input: &str, sep: char) -> Option<(&str, &str)> {
        let mut s = input.split_terminator(sep);
        Some((s.next()?, s.next()?))
    }
}

// impl Emotes {
//     // TODO look up the emote id https://twitchemotes.com/emotes/{id}
//     // https://static-cdn.jtvnw.net/emoticons/v1/{id}/3.0
//     pub fn lookup(&self) -> String {
//         unimplemented!()
//     }
// }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[allow(clippy::unreadable_literal)]
    fn parse_emotes() {
        macro_rules! emote {
            ($id:expr, $($r:expr),* $(,)?) => {
                Emotes {
                    id: $id,
                    ranges: vec![$($r),*]
                }
            };
        }

        let inputs = &[
            (
                "25:0-4,6-10,12-16",
                vec![
                    emote!(25, (0..4), (6..10), (12..16)), //
                ],
            ),
            (
                "25:0-4",
                vec![
                    emote!(25, (0..4)), //
                ],
            ),
            (
                "1077966:0-6/25:8-12",
                vec![
                    emote!(1077966, (0..6)), //
                    emote!(25, (8..12)),     //
                ],
            ),
            (
                "25:0-4,6-10/33:12-19",
                vec![
                    emote!(25, (0..4), (6..10)), //
                    emote!(33, (12..19)),        //
                ],
            ),
            (
                "25:0-4,15-19/33:6-13",
                vec![
                    emote!(25, (0..4), (15..19)), //
                    emote!(33, (6..13)),          //
                ],
            ),
            (
                "33:0-7/25:9-13,15-19",
                vec![
                    emote!(33, (0..7)),            //
                    emote!(25, (9..13), (15..19)), //
                ],
            ),
        ];

        for (input, expect) in inputs {
            let emotes = Emotes::parse(&input).collect::<Vec<_>>();
            assert_eq!(emotes.len(), expect.len());
            assert_eq!(emotes, *expect);
        }
    }
}