use crate::console::{Console, ConsoleOptions};
use crate::protocol::Renderable;
use crate::segment::Segment;
use crate::style::StyleType;
const VARIANT_EMOJI: &str = "\u{fe0f}";
const VARIANT_TEXT: &str = "\u{fe0e}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmojiVariant {
Emoji,
Text,
}
impl EmojiVariant {
pub fn parse(name: &str) -> Option<EmojiVariant> {
match name {
"emoji" => Some(EmojiVariant::Emoji),
"text" => Some(EmojiVariant::Text),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
EmojiVariant::Emoji => "emoji",
EmojiVariant::Text => "text",
}
}
fn selector(self) -> &'static str {
match self {
EmojiVariant::Emoji => VARIANT_EMOJI,
EmojiVariant::Text => VARIANT_TEXT,
}
}
}
pub fn replace(text: &str) -> String {
replace_with_variant(text, None)
}
pub fn replace_with_variant(text: &str, default_variant: Option<EmojiVariant>) -> String {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let mut out = String::with_capacity(text.len());
let mut index = 0;
while index < len {
if chars[index] == ':' {
let mut end = index + 1;
while end < len && !is_python_space(chars[end]) && chars[end] != ':' {
end += 1;
}
if end < len && chars[end] == ':' {
let body: String = chars[index + 1..end].iter().collect();
match lookup(&body, default_variant) {
Some(replacement) => out.push_str(&replacement),
None => out.extend(&chars[index..=end]),
}
index = end + 1;
continue;
}
}
out.push(chars[index]);
index += 1;
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoEmoji(pub String);
impl std::fmt::Display for NoEmoji {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for NoEmoji {}
#[derive(Debug, Clone)]
pub struct Emoji {
name: String,
style: StyleType,
variant: Option<EmojiVariant>,
character: String,
}
impl Emoji {
pub fn new(name: &str) -> Result<Emoji, NoEmoji> {
let character = emoji_code(name)
.ok_or_else(|| NoEmoji(format!("No emoji called {}", crate::pretty::str_repr(name))))?;
Ok(Emoji {
name: name.to_string(),
style: StyleType::from("none"),
variant: None,
character: character.to_string(),
})
}
pub fn style(mut self, style: impl Into<StyleType>) -> Self {
self.style = style.into();
self
}
pub fn variant(mut self, variant: EmojiVariant) -> Self {
if self.variant.is_none() {
self.character.push_str(variant.selector());
} else {
let base = emoji_code(&self.name).unwrap_or_default();
self.character = format!("{base}{}", variant.selector());
}
self.variant = Some(variant);
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn replace(text: &str) -> String {
replace(text)
}
}
impl std::fmt::Display for Emoji {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.character)
}
}
impl Renderable for Emoji {
fn rich_render(&self, console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
let style = console.get_style(&self.style).unwrap_or_default();
vec![Segment::new(self.character.clone(), Some(style))]
}
}
fn is_python_space(c: char) -> bool {
c.is_whitespace() || ('\u{1c}'..='\u{1f}').contains(&c)
}
fn lookup(body: &str, default_variant: Option<EmojiVariant>) -> Option<String> {
let default = default_variant.map_or("", EmojiVariant::selector);
let (name, variant) = if let Some(name) = body.strip_suffix("-emoji") {
(name, VARIANT_EMOJI)
} else if let Some(name) = body.strip_suffix("-text") {
(name, VARIANT_TEXT)
} else {
(body, default)
};
emoji_code(&name.to_lowercase()).map(|code| format!("{code}{variant}"))
}
fn emoji_code(name: &str) -> Option<&'static str> {
crate::emoji_codes::emoji_code(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replaces_known_codes() {
assert_eq!(replace(":rocket:"), "\u{1f680}");
assert_eq!(replace("hello :thumbs_up: world"), "hello \u{1f44d} world");
assert_eq!(replace(":rocket: to :moon:"), "\u{1f680} to \u{1f314}");
}
#[test]
fn leaves_unknown_and_bare_colons() {
assert_eq!(replace(":not_a_real_emoji:"), ":not_a_real_emoji:");
assert_eq!(replace("no emoji"), "no emoji");
assert_eq!(replace("ratio 3:4 done"), "ratio 3:4 done");
}
#[test]
fn emoji_renderable_matches_upstream() {
let rocket = Emoji::new("rocket").unwrap();
assert_eq!(rocket.to_string(), "\u{1f680}");
assert_eq!(
rocket.clone().variant(EmojiVariant::Text).to_string(),
"\u{1f680}\u{fe0e}"
);
assert_eq!(
Emoji::new("nope").unwrap_err().to_string(),
"No emoji called 'nope'"
);
assert!(Emoji::new("Rocket").is_err());
let console = crate::Console::builder()
.force_terminal(true)
.color_system(Some(crate::ColorSystem::Truecolor))
.width(10)
.build();
let out = console.render_to_string(&rocket.style("red"));
assert_eq!(out, "\x1b[31m\u{1f680}\x1b[0m");
}
#[test]
fn variant_selectors() {
assert_eq!(replace(":rocket-emoji:"), "\u{1f680}\u{fe0f}");
assert_eq!(replace(":rocket-text:"), "\u{1f680}\u{fe0e}");
}
#[test]
fn default_variant_and_case_sensitive_suffix() {
assert_eq!(replace(":rocket-EMOJI:"), ":rocket-EMOJI:");
assert_eq!(replace(":ROCKET-text:"), "\u{1f680}\u{fe0e}");
assert_eq!(
replace_with_variant(":rocket: :rocket-text:", Some(EmojiVariant::Emoji)),
"\u{1f680}\u{fe0f} \u{1f680}\u{fe0e}"
);
assert_eq!(
replace_with_variant(":rocket:", Some(EmojiVariant::Text)),
"\u{1f680}\u{fe0e}"
);
}
#[test]
fn scanning_resumes_after_every_match() {
assert_eq!(replace(" -:็[ba:b: x"), " -:็[ba:b: x");
assert_eq!(replace("::rocket:"), "::rocket:");
assert_eq!(replace(":x:rocket:"), "\u{274c}rocket:");
assert_eq!(replace(":rocket:rocket:"), "\u{1f680}rocket:");
assert_eq!(replace(":ROCKET:"), "\u{1f680}");
assert_eq!(replace("a:\u{1c}rocket:"), "a:\u{1c}rocket:");
assert_eq!(replace(":\u{212a}rocket:"), ":\u{212a}rocket:");
}
}