use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmojiVariant {
Emoji,
Text,
}
impl EmojiVariant {
#[must_use]
pub const fn selector(self) -> &'static str {
match self {
Self::Emoji => "\u{FE0F}",
Self::Text => "\u{FE0E}",
}
}
}
static EMOJI_MAP: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
let mut map = HashMap::new();
for line in include_str!("emoji_codes.tsv").lines() {
let line = line.trim_end();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((name, emoji)) = line.split_once('\t') else {
continue;
};
if name.is_empty() || emoji.is_empty() {
continue;
}
map.insert(name, emoji);
}
map
});
#[must_use]
pub fn get(name: &str) -> Option<&'static str> {
EMOJI_MAP.get(name).copied()
}
#[must_use]
pub fn replace(text: &str, default_variant: Option<EmojiVariant>) -> Cow<'_, str> {
if !text.as_bytes().contains(&b':') {
return Cow::Borrowed(text);
}
let default_selector = default_variant.map_or("", EmojiVariant::selector);
let mut cursor = 0;
let mut search = 0;
let mut out: Option<String> = None;
while let Some(rel_start) = text[search..].find(':') {
let start = search + rel_start;
if let Some((end, replacement)) = try_replace_at(text, start, default_selector) {
if let Some(buf) = out.as_mut() {
buf.push_str(&text[cursor..start]);
buf.push_str(&replacement);
} else {
let mut buf = String::with_capacity(text.len());
buf.push_str(&text[..start]);
buf.push_str(&replacement);
out = Some(buf);
}
cursor = end + 1;
search = cursor;
} else {
search = start + 1;
}
}
match out {
None => Cow::Borrowed(text),
Some(mut buf) => {
buf.push_str(&text[cursor..]);
Cow::Owned(buf)
}
}
}
fn try_replace_at(
text: &str,
start: usize,
default_selector: &'static str,
) -> Option<(usize, String)> {
debug_assert_eq!(text.as_bytes()[start], b':');
let bytes = text.as_bytes();
let mut i = start + 1;
let mut end = None;
while i < bytes.len() {
let b = bytes[i];
if b == b':' {
end = Some(i);
break;
}
if b.is_ascii_whitespace() {
break;
}
i += 1;
}
let end = end?;
let inner = &text[start + 1..end];
let (name, selector) = if let Some(name) = inner.strip_suffix("-emoji") {
(name, EmojiVariant::Emoji.selector())
} else if let Some(name) = inner.strip_suffix("-text") {
(name, EmojiVariant::Text.selector())
} else {
(inner, default_selector)
};
let emoji_name = name.to_lowercase();
let emoji = EMOJI_MAP.get(emoji_name.as_str()).copied()?;
let mut replacement = String::with_capacity(emoji.len() + selector.len());
replacement.push_str(emoji);
replacement.push_str(selector);
Some((end, replacement))
}
#[cfg(test)]
mod tests {
use super::{EmojiVariant, replace};
#[test]
fn test_replace_basic() {
assert_eq!(replace("hi :smile:", None), "hi 😄");
}
#[test]
fn test_replace_lowercases_name() {
assert_eq!(replace("hi :SMILE:", None), "hi 😄");
}
#[test]
fn test_replace_variant_text() {
assert_eq!(
replace(":smile-text:", None),
format!("😄{}", EmojiVariant::Text.selector())
);
}
#[test]
fn test_replace_variant_emoji() {
assert_eq!(
replace(":smile-emoji:", None),
format!("😄{}", EmojiVariant::Emoji.selector())
);
}
#[test]
fn test_replace_default_variant() {
assert_eq!(
replace(":smile:", Some(EmojiVariant::Emoji)),
format!("😄{}", EmojiVariant::Emoji.selector())
);
}
#[test]
fn test_unknown_passthrough() {
assert_eq!(
replace("hi :definitely_not_real:", None),
"hi :definitely_not_real:"
);
}
#[test]
fn test_whitespace_breaks_match() {
assert_eq!(replace("hi :smile :", None), "hi :smile :");
}
#[test]
fn test_variant_must_be_lowercase() {
assert_eq!(replace(":smile-EMOJI:", None), ":smile-EMOJI:");
}
}