use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Part {
Text {
text: String,
},
Emote {
id: String,
name: String,
count: u32,
},
}
pub fn parse(content: &str, combine: bool) -> Vec<Part> {
let mut parts: Vec<Part> = Vec::new();
let mut rest = content;
let mut between = String::new();
while let Some(start) = rest.find("[emote:") {
let (before, token) = rest.split_at(start);
let Some((id, name, len)) = emote_token(token) else {
between.push_str(&rest[..start + 1]);
rest = &rest[start + 1..];
continue;
};
between.push_str(before);
let same_run = combine
&& between.trim().is_empty()
&& matches!(parts.last(), Some(Part::Emote { id: last, .. }) if *last == id);
if same_run {
if let Some(Part::Emote { count, .. }) = parts.last_mut() {
*count += 1;
}
} else {
if !between.is_empty() {
parts.push(Part::Text { text: std::mem::take(&mut between) });
}
parts.push(Part::Emote { id, name, count: 1 });
}
between.clear();
rest = &token[len..];
}
between.push_str(rest);
if !between.is_empty() {
parts.push(Part::Text { text: between });
}
parts
}
fn emote_token(s: &str) -> Option<(String, String, usize)> {
let body = s.strip_prefix("[emote:")?;
let colon = body.find(':')?;
let id = &body[..colon];
if id.is_empty() || !id.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let after = &body[colon + 1..];
let close = after.find(']')?;
let name = &after[..close];
Some((id.to_owned(), name.to_owned(), "[emote:".len() + colon + 1 + close + 1))
}
pub fn emote_url(id: &str) -> String {
format!("https://files.kick.com/emotes/{}/fullsize", crate::http::encode(id))
}
#[cfg(test)]
mod tests {
use super::*;
fn emote(id: &str, name: &str, count: u32) -> Part {
Part::Emote { id: id.into(), name: name.into(), count }
}
fn text(t: &str) -> Part {
Part::Text { text: t.into() }
}
#[test]
fn splits_text_and_emotes() {
assert_eq!(
parse("hi [emote:37226:KEKW] there", false),
vec![text("hi "), emote("37226", "KEKW", 1), text(" there")]
);
assert_eq!(parse("", false), vec![]);
assert_eq!(parse("[emote:x:no] [emote:1:", false), vec![text("[emote:x:no] [emote:1:")]);
}
#[test]
fn combines_runs_of_one_emote() {
assert_eq!(
parse("[emote:1:A] [emote:1:A][emote:1:B] [emote:2:C] x [emote:2:C]", true),
vec![emote("1", "A", 3), text(" "), emote("2", "C", 1), text(" x "), emote("2", "C", 1)]
);
assert_eq!(parse("[emote:1:A] [emote:1:A]", false).len(), 3);
}
}