text_components 0.1.6

A Rust implementation of Minecraft's Text Components
Documentation
use crate::{
    TextComponent,
    content::{Content, Object},
    format::{Color, Format},
    interactivity::{ClickEvent, Interactivity},
    resolving::{BuildTarget, NoResolutor, TextResolutor},
};
use colored::{ColoredString, Colorize};
use rand::random_range;
use std::{
    borrow::Cow,
    fmt::{self, Debug, Display, Formatter, Pointer},
};
use supports_hyperlinks::supports_hyperlinks;

const OBFUSCATION_CHARS: [char; 822] = [
    '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3',
    '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F',
    'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
    'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
    'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '¡',
    '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '®', '¯', '°', '±', '²', '³', '´', 'µ',
    '', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È',
    'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û',
    'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î',
    'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', 'Ā', 'ā',
    'Ă', 'ă', 'Ą', 'ą', 'Ć', 'ć', 'Ĉ', 'ĉ', 'Ċ', 'ċ', 'Č', 'č', 'Ď', 'ď', 'Đ', 'đ', 'Ē', 'ē', 'Ĕ',
    'ĕ', 'Ė', 'ė', 'Ę', 'ę', 'Ě', 'ě', 'Ĝ', 'ĝ', 'Ğ', 'ğ', 'Ġ', 'ġ', 'Ģ', 'ģ', 'Ĥ', 'ĥ', 'Ħ', 'ħ',
    'Ĩ', 'ĩ', 'Ī', 'ī', 'Ĭ', 'ĭ', 'Į', 'į', 'İ', 'ı', 'IJ', 'ij', 'Ĵ', 'ĵ', 'Ķ', 'ķ', 'ĸ', 'Ĺ', 'ĺ',
    'Ļ', 'ļ', 'Ľ', 'ľ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'Ń', 'ń', 'Ņ', 'ņ', 'Ň', 'ň', 'ʼn', 'Ŋ', 'ŋ', 'Ō', 'ō',
    'Ŏ', 'ŏ', 'Ő', 'ő', 'Œ', 'œ', 'Ŕ', 'ŕ', 'Ŗ', 'ŗ', 'Ř', 'ř', 'Ś', 'ś', 'Ŝ', 'ŝ', 'Ş', 'ş', 'Š',
    'š', 'Ţ', 'ţ', 'Ť', 'ť', 'Ŧ', 'ŧ', 'Ũ', 'ũ', 'Ū', 'ū', 'Ŭ', 'ŭ', 'Ů', 'ů', 'Ű', 'ű', 'Ų', 'ų',
    'Ŵ', 'ŵ', 'Ŷ', 'ŷ', 'Ÿ', 'Ź', 'ź', 'Ż', 'ż', 'Ž', 'ž', 'ſ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƅ', 'ƅ', 'Ɔ',
    'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'ƍ', 'Ǝ', 'Ə', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'Ɣ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ',
    'ƚ', 'ƛ', 'Ɯ', 'Ɲ', 'ƞ', 'Ɵ', 'Ơ', 'ơ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'Ʀ', 'Ƨ', 'ƨ', 'Ʃ', 'ƪ', 'ƫ', 'Ƭ',
    'ƭ', 'Ʈ', 'Ư', 'ư', 'Ʊ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'Ʒ', 'Ƹ', 'ƹ', 'ƺ', 'ƻ', 'Ƽ', 'ƽ', 'ƾ', 'ƿ',
    'ǀ', 'ǁ', 'ǂ', 'ǃ', 'DŽ', 'Dž', 'dž', 'LJ', 'Lj', 'lj', 'NJ', 'Nj', 'nj', 'Ǎ', 'ǎ', 'Ǐ', 'ǐ', 'Ǒ', 'ǒ',
    'Ǔ', 'ǔ', 'Ǖ', 'ǖ', 'Ǘ', 'ǘ', 'Ǚ', 'ǚ', 'Ǜ', 'ǜ', 'ǝ', 'Ǟ', 'ǟ', 'Ǡ', 'ǡ', 'Ǣ', 'ǣ', 'Ǥ', 'ǥ',
    'Ǧ', 'ǧ', 'Ǩ', 'ǩ', 'Ǫ', 'ǫ', 'Ǭ', 'ǭ', 'Ǯ', 'ǯ', 'ǰ', 'DZ', 'Dz', 'dz', 'Ǵ', 'ǵ', 'Ƕ', 'Ƿ', 'Ǹ',
    'ǹ', 'Ǻ', 'ǻ', 'Ǽ', 'ǽ', 'Ǿ', 'ǿ', 'Ȁ', 'ȁ', 'Ȃ', 'ȃ', 'Ȅ', 'ȅ', 'Ȇ', 'ȇ', 'Ȉ', 'ȉ', 'Ȋ', 'ȋ',
    'Ȍ', 'ȍ', 'Ȏ', 'ȏ', 'Ȑ', 'ȑ', 'Ȓ', 'ȓ', 'Ȕ', 'ȕ', 'Ȗ', 'ȗ', 'Ș', 'ș', 'Ț', 'ț', 'Ȝ', 'ȝ', 'Ȟ',
    'ȟ', 'Ƞ', 'ȡ', 'Ȣ', 'ȣ', 'Ȥ', 'ȥ', 'Ȧ', 'ȧ', 'Ȩ', 'ȩ', 'Ȫ', 'ȫ', 'Ȭ', 'ȭ', 'Ȯ', 'ȯ', 'Ȱ', 'ȱ',
    'Ȳ', 'ȳ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ɂ', 'ɂ', 'Ƀ', 'Ʉ',
    'Ʌ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɋ', 'ɋ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɐ', 'ɑ', 'ɒ', 'ɓ', 'ɔ', 'ɕ', 'ɖ', 'ɗ',
    'ɘ', 'ə', 'ɚ', 'ɛ', 'ɜ', 'ɝ', 'ɞ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɣ', 'ɤ', 'ɥ', 'ɦ', 'ɧ', 'ɨ', 'ɩ', 'ɪ',
    'ɫ', 'ɬ', 'ɭ', 'ɮ', 'ɯ', 'ɰ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɵ', 'ɶ', 'ɷ', 'ɸ', 'ɹ', 'ɺ', 'ɻ', 'ɼ', 'ɽ',
    'ɾ', 'ɿ', 'ʀ', 'ʁ', 'ʂ', 'ʃ', 'ʄ', 'ʅ', 'ʆ', 'ʇ', 'ʈ', 'ʉ', 'ʊ', 'ʋ', 'ʌ', 'ʍ', 'ʎ', 'ʏ', 'ʐ',
    'ʑ', 'ʒ', 'ʓ', 'ʔ', 'ʕ', 'ʖ', 'ʗ', 'ʘ', 'ʙ', 'ʚ', 'ʛ', 'ʜ', 'ʝ', 'ʞ', 'ʟ', 'ʠ', 'ʡ', 'ʢ', 'ʣ',
    'ʤ', 'ʥ', 'ʦ', 'ʧ', 'ʨ', 'ʩ', 'ʪ', 'ʫ', 'ʬ', 'ʭ', 'ʮ', 'ʯ', 'Ά', '·', 'Έ', 'Ή', 'Ί', '΋', 'Ό',
    '΍', 'Ύ', 'Ώ', 'ΐ', 'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ', 'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο',
    'Π', 'Ρ', '΢', 'Σ', 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω', 'Ϊ', 'Ϋ', 'ά', 'έ', 'ή', 'ί', 'ΰ', 'α', 'β',
    'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'ς', 'σ', 'τ', 'υ',
    'φ', 'χ', 'ψ', 'ω', 'ϊ', 'ϋ', 'ό', 'ύ', 'ώ', 'Ϗ', 'ϐ', 'ϑ', 'ϒ', 'ϓ', 'ϔ', 'ϕ', 'ϖ', 'ϗ', 'Ϙ',
    'ϙ', 'Ϛ', 'ϛ', 'Ϝ', 'ϝ', 'Ϟ', 'ϟ', 'Ϡ', 'ϡ', 'Ϣ', 'ϣ', 'Ϥ', 'ϥ', 'Ϧ', 'ϧ', 'Ϩ', 'ϩ', 'Ϫ', 'ϫ',
    'Ϭ', 'ϭ', 'Ϯ', 'ϯ', 'ϰ', 'ϱ', 'ϲ', 'ϳ', 'ϴ', 'ϵ', '϶', 'Ϸ', 'ϸ', 'Ϲ', 'Ϻ', 'ϻ', 'ϼ', 'Ͻ', 'Ͼ',
    'Ͽ', 'Ѐ', 'Ё', 'Ђ', 'Ѓ', 'Є', 'Ѕ', 'І', 'Ї', 'Ј', 'Љ', 'Њ', 'Ћ', 'Ќ', 'Ѝ', 'Ў', 'Џ', 'А', 'Б',
    'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф',
    'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я', 'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з',
    'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ',
    'ы', 'ь', 'э', 'ю', 'я',
];

pub struct TextBuilder;
impl TextBuilder {
    fn stringify_content<R: TextResolutor + ?Sized, S: BuildTarget>(
        target: &S,
        resolutor: &R,
        component: &TextComponent,
    ) -> S::Result
    where
        S::Result: From<String> + ToString + Display,
    {
        match &component.content {
            Content::Text { text } => text.to_string().into(),
            Content::Translate(message) => {
                let translated = match resolutor.translate(&message.key) {
                    Some(t) => t,
                    None => match &message.fallback {
                        Some(f) => return f.to_string().into(),
                        None => return format!("[Translation: {}]", message.key).into(),
                    },
                };
                let parts = resolutor.split_translation(translated);
                let mut builded_parts = vec![];
                for (part, pos) in parts {
                    let component_part = TextComponent {
                        content: part.into(),
                        format: component.format.clone(),
                        ..TextComponent::new()
                    };
                    builded_parts.push(
                        target
                            .build_component(resolutor, &component_part)
                            .to_string(),
                    );
                    if pos != 0
                        && let Some(args) = &message.args
                        && pos <= args.len()
                        && let Some(arg) = args.get(pos - 1)
                    {
                        let arg_part = TextComponent {
                            content: arg.content.clone(),
                            children: arg.children.clone(),
                            format: arg.format.mix(&component.format),
                            interactions: arg.interactions.clone(),
                        };
                        builded_parts
                            .push(target.build_component(resolutor, &arg_part).to_string());
                    }
                }
                return builded_parts.concat().into();
            }
            Content::Keybind { keybind } => format!("[Keybind: {}]", keybind).into(),
            Content::Object(Object::Atlas { sprite, .. }) => format!("[Object: {}]", sprite).into(),
            Content::Object(Object::Player { player, .. }) => {
                if let Some(name) = &player.name {
                    return format!("[Head: {}]", name).into();
                }
                if let Some(id) = &player.id {
                    return format!("[Head: {:?}]", id).into();
                }
                String::from("[Head]").into()
            }
            Content::Resolvable(_) => String::from("[Resolvable]").into(), // Just in case ;)
            #[cfg(feature = "custom")]
            Content::Custom { .. } => String::from("[Custom]").into(),
        }
    }
}
impl BuildTarget for TextBuilder {
    type Result = String;
    fn build_component<R: TextResolutor + ?Sized>(
        &self,
        resolutor: &R,
        component: &TextComponent,
    ) -> String {
        Self::stringify_content(self, resolutor, &component)
            + &component
                .children
                .iter()
                .map(|child| self.build_component(resolutor, child))
                .collect::<Vec<String>>()
                .concat()
    }
}

pub struct PrettyTextBuilder;
impl BuildTarget for PrettyTextBuilder {
    type Result = ColoredString;
    fn build_component<R: TextResolutor + ?Sized>(
        &self,
        resolutor: &R,
        component: &TextComponent,
    ) -> ColoredString {
        let mut final_text = TextBuilder::stringify_content(self, resolutor, &component);

        if let Content::Translate(_) = component.content {
            return format!(
                "{}{}",
                final_text,
                component
                    .children
                    .iter()
                    .map(|child| {
                        let child = TextComponent {
                            content: child.content.clone(),
                            children: child.children.clone(),
                            format: child.format.mix(&component.format),
                            interactions: child.interactions.clone(),
                        };
                        self.build_component(resolutor, &child).to_string()
                    })
                    .collect::<Vec<String>>()
                    .concat()
            )
            .into();
        }

        if let Some(true) = component.format.obfuscated {
            let obfuscated = final_text
                .chars()
                .into_iter()
                .map(|char| {
                    if !char.is_whitespace() && !char.is_control() {
                        return OBFUSCATION_CHARS[random_range(0..822)];
                    }
                    char
                })
                .collect::<String>();
            final_text = ColoredString::from(obfuscated);
        }
        if let Some(color) = &component.format.color {
            final_text = color.colorize_text(final_text.to_string());
        }
        if let Some(true) = component.format.bold {
            final_text = final_text.bold();
        }
        if let Some(true) = component.format.italic {
            final_text = final_text.italic();
        }
        if let Some(true) = component.format.underlined {
            final_text = final_text.underline();
        }
        if let Some(true) = component.format.strikethrough {
            final_text = final_text.strikethrough();
        }
        if let Some(color) = component.format.shadow_color {
            final_text = final_text.on_truecolor(
                ((color >> 16) & 0xFF) as u8,
                ((color >> 8) & 0xFF) as u8,
                (color & 0xFF) as u8,
            );
        }
        if supports_hyperlinks()
            && let Some(ClickEvent::OpenUrl { url }) = &component.interactions.click
        {
            final_text = format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", url, final_text).into();
        }

        format!(
            "{}{}",
            final_text,
            component
                .children
                .iter()
                .map(|child| {
                    let child = TextComponent {
                        content: child.content.clone(),
                        children: child.children.clone(),
                        format: child.format.mix(&component.format),
                        interactions: child.interactions.clone(),
                    };
                    self.build_component(resolutor, &child).to_string()
                })
                .collect::<Vec<String>>()
                .concat()
        )
        .into()
    }
}

impl TextComponent {
    pub fn to_plain<R: TextResolutor + ?Sized>(&self, resolutor: &R) -> String {
        self.build(resolutor, TextBuilder)
    }
    pub fn to_pretty<R: TextResolutor + ?Sized>(&self, resolutor: &R) -> ColoredString {
        self.build(resolutor, PrettyTextBuilder)
    }
}

static mut DISPLAY_RESOLUTOR: &dyn TextResolutor = &NoResolutor;
static mut INITIALIZED: bool = false;

pub fn set_display_resolutor<T: TextResolutor>(resolutor: &'static T) {
    unsafe {
        if !INITIALIZED {
            DISPLAY_RESOLUTOR = resolutor;
            INITIALIZED = true;
        }
    }
}

impl Display for TextComponent {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", unsafe { self.to_plain(DISPLAY_RESOLUTOR) })
    }
}

/// Clearly a Pointer, not 'p' because of pretty, OF COURSE
impl Pointer for TextComponent {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", unsafe { self.to_pretty(DISPLAY_RESOLUTOR) })
    }
}

impl Debug for TextComponent {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let mut debug = f.debug_struct("TextComponent");
        debug.field("content", &self.content);
        if !self.format.is_none() {
            debug.field("format", &self.format);
        }
        if !self.interactions.is_none() {
            debug.field("interactions", &self.interactions);
        }
        if !self.children.is_empty() {
            debug.field("children", &self.children);
        }
        debug.finish()
    }
}

impl Debug for Content {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::Text { text } => Debug::fmt(&text, f),
            Self::Keybind { keybind } => f.debug_tuple("Keybind").field(keybind).finish(),
            #[cfg(feature = "custom")]
            Self::Custom(arg0) => Debug::fmt(&arg0, f),
            Self::Translate(arg0) => Debug::fmt(&arg0, f),
            Self::Object(arg0) => Debug::fmt(&arg0, f),
            Self::Resolvable(arg0) => Debug::fmt(&arg0, f),
        }
    }
}

impl Debug for Format {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        if let Some(Color::White) = self.color
            && let Some(Cow::Borrowed("minecraft:default")) = self.font
            && let Some(false) = self.bold
            && let Some(false) = self.italic
            && let Some(false) = self.underlined
            && let Some(false) = self.strikethrough
            && let Some(false) = self.obfuscated
            && let None = self.shadow_color
        {
            return write!(f, "{{ RESET }}");
        }

        let mut items = vec![];
        if let Some(color) = &self.color {
            items.push(match color {
                crate::format::Color::Aqua => format!(" color: Aqua"),
                crate::format::Color::Black => format!(" color: Black"),
                crate::format::Color::Blue => format!(" color: Blue"),
                crate::format::Color::DarkAqua => format!(" color: Dark Aqua"),
                crate::format::Color::DarkBlue => format!(" color: Dark Blue"),
                crate::format::Color::DarkGray => format!(" color: Dark Gray"),
                crate::format::Color::DarkGreen => format!(" color: Dark Green"),
                crate::format::Color::DarkPurple => format!(" color: Dark Purple"),
                crate::format::Color::DarkRed => format!(" color: Dark Red"),
                crate::format::Color::Gold => format!(" color: Gold"),
                crate::format::Color::Gray => format!(" color: Gray"),
                crate::format::Color::Green => format!(" color: Green"),
                crate::format::Color::LightPurple => format!(" color: Light Purple"),
                crate::format::Color::Red => format!(" color: Red"),
                crate::format::Color::White => format!(" color: White"),
                crate::format::Color::Yellow => format!(" color: Yellow"),
                crate::format::Color::Rgb(r, g, b) => format!(" color: [{r}, {g}, {b}]"),
            });
        }
        if let Some(font) = &self.font
            && font != "minecraft:default"
        {
            items.push(format!(" font: \"{font}\""));
        }
        if let Some(true) = self.bold {
            items.push(format!(" bold"));
        }
        if let Some(true) = self.italic {
            items.push(format!(" italic"));
        }
        if let Some(true) = self.underlined {
            items.push(format!(" underlined"));
        }
        if let Some(true) = self.strikethrough {
            items.push(format!(" strikethrough"));
        }
        if let Some(true) = self.obfuscated {
            items.push(format!(" obfuscated"));
        }
        if let Some(color) = self.shadow_color {
            items.push(format!(
                " [{}, {}, {}, {}]",
                (color >> 16) & 255,
                (color >> 8) & 255,
                color & 255,
                (color >> 24) & 255
            ));
        }

        write!(f, "{{{} }}", items.join(","))
    }
}

impl Debug for Interactivity {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let mut debug = f.debug_map();
        if self.insertion.is_some() {
            debug.entry(&"insertion", &self.insertion);
        }
        if self.click.is_some() {
            debug.entry(&"click_event", &self.click);
        }
        if self.hover.is_some() {
            debug.entry(&"hover_event", &self.hover);
        }
        debug.finish()
    }
}