gilt 0.5.0

A fast, rich terminal formatting library — Rust port of Python's rich
Documentation
//! Emoji replacement in text strings.
//!
//! Replaces `:emoji_name:` patterns in text with actual Unicode emoji characters.

use regex::Regex;
use std::borrow::Cow;
use std::sync::LazyLock;

use crate::emoji_codes::EMOJI;

static EMOJI_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r":(\S*?)(?:(?:\-)(emoji|text))?:").unwrap());

/// Replace `:emoji_name:` patterns in text with corresponding Unicode emoji.
///
/// Supports optional variant suffixes:
/// - `:name-text:` appends the text presentation selector (U+FE0E)
/// - `:name-emoji:` appends the emoji presentation selector (U+FE0F)
///
/// If `default_variant` is provided ("text" or "emoji"), that variant selector
/// is appended to all replacements that don't specify one explicitly.
///
/// Unknown emoji names are left unchanged (e.g. `:unknown:` stays as-is).
pub fn emoji_replace<'a>(text: &'a str, default_variant: Option<&str>) -> Cow<'a, str> {
    let default_variant_code = match default_variant {
        Some("text") => "\u{FE0E}",
        Some("emoji") => "\u{FE0F}",
        _ => "",
    };

    EMOJI_RE.replace_all(text, |caps: &regex::Captures| {
        let full_match = caps.get(0).unwrap().as_str();
        let emoji_name = caps.get(1).unwrap().as_str().to_lowercase();
        let variant = caps.get(2).map(|m| m.as_str());

        match EMOJI.get(emoji_name.as_str()) {
            Some(emoji_char) => {
                let variant_code = match variant {
                    Some("text") => "\u{FE0E}",
                    Some("emoji") => "\u{FE0F}",
                    _ => default_variant_code,
                };
                format!("{}{}", emoji_char, variant_code)
            }
            None => full_match.to_string(),
        }
    })
}

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

    #[test]
    fn test_simple_replacement() {
        let result = emoji_replace(":heart:", None);
        assert_eq!(result, "\u{2764}");
    }

    #[test]
    fn test_unknown_emoji_unchanged() {
        let result = emoji_replace(":unknown_xyz:", None);
        assert_eq!(result, ":unknown_xyz:");
    }

    #[test]
    fn test_variant_text() {
        let result = emoji_replace(":heart-text:", None);
        assert_eq!(result, "\u{2764}\u{FE0E}");
    }

    #[test]
    fn test_variant_emoji() {
        let result = emoji_replace(":heart-emoji:", None);
        assert_eq!(result, "\u{2764}\u{FE0F}");
    }

    #[test]
    fn test_multiple_emojis() {
        let result = emoji_replace("Hello :heart: world :thumbs_up:!", None);
        assert_eq!(result, "Hello \u{2764} world \u{1F44D}!");
    }

    #[test]
    fn test_no_emojis_passthrough() {
        let result = emoji_replace("Hello, world!", None);
        assert_eq!(result, "Hello, world!");
    }

    #[test]
    fn test_default_variant_text() {
        let result = emoji_replace(":heart:", Some("text"));
        assert_eq!(result, "\u{2764}\u{FE0E}");
    }

    #[test]
    fn test_default_variant_emoji() {
        let result = emoji_replace(":heart:", Some("emoji"));
        assert_eq!(result, "\u{2764}\u{FE0F}");
    }

    #[test]
    fn test_explicit_variant_overrides_default() {
        // Explicit -text should use text variant even when default is emoji
        let result = emoji_replace(":heart-text:", Some("emoji"));
        assert_eq!(result, "\u{2764}\u{FE0E}");
    }

    #[test]
    fn test_empty_string() {
        let result = emoji_replace("", None);
        assert_eq!(result, "");
    }

    #[test]
    fn test_case_insensitive_name() {
        let result = emoji_replace(":HEART:", None);
        assert_eq!(result, "\u{2764}");
    }
}