azalea_chat/
style.rs

1use std::{collections::HashMap, fmt, sync::LazyLock};
2
3#[cfg(feature = "azalea-buf")]
4use azalea_buf::AzBuf;
5use serde::{Serialize, Serializer, ser::SerializeStruct};
6use serde_json::Value;
7#[cfg(feature = "simdnbt")]
8use simdnbt::owned::{NbtCompound, NbtTag};
9
10#[derive(Clone, PartialEq, Eq, Debug, Hash)]
11pub struct TextColor {
12    pub value: u32,
13    pub name: Option<String>,
14}
15
16impl Serialize for TextColor {
17    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
18    where
19        S: Serializer,
20    {
21        serializer.serialize_str(&self.serialize())
22    }
23}
24
25#[cfg(feature = "simdnbt")]
26impl simdnbt::ToNbtTag for TextColor {
27    fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
28        NbtTag::String(self.serialize().into())
29    }
30}
31
32impl TextColor {
33    pub fn parse(value: String) -> Option<TextColor> {
34        if value.starts_with('#') {
35            let n = value.chars().skip(1).collect::<String>();
36            let n = u32::from_str_radix(&n, 16).ok()?;
37            return Some(TextColor::from_rgb(n));
38        }
39        let color_option = NAMED_COLORS.get(&value.to_ascii_uppercase());
40        if let Some(color) = color_option {
41            return Some(color.clone());
42        }
43        None
44    }
45
46    fn from_rgb(value: u32) -> TextColor {
47        TextColor { value, name: None }
48    }
49}
50
51static LEGACY_FORMAT_TO_COLOR: LazyLock<HashMap<&'static ChatFormatting, TextColor>> =
52    LazyLock::new(|| {
53        let mut legacy_format_to_color = HashMap::new();
54        for formatter in &ChatFormatting::FORMATTERS {
55            if !formatter.is_format() && *formatter != ChatFormatting::Reset {
56                legacy_format_to_color.insert(
57                    formatter,
58                    TextColor {
59                        value: formatter.color().unwrap(),
60                        name: Some(formatter.name().to_string()),
61                    },
62                );
63            }
64        }
65        legacy_format_to_color
66    });
67static NAMED_COLORS: LazyLock<HashMap<String, TextColor>> = LazyLock::new(|| {
68    let mut named_colors = HashMap::new();
69    for color in LEGACY_FORMAT_TO_COLOR.values() {
70        named_colors.insert(color.name.clone().unwrap(), color.clone());
71    }
72    named_colors
73});
74
75pub struct Ansi {}
76impl Ansi {
77    pub const BOLD: &'static str = "\u{1b}[1m";
78    pub const ITALIC: &'static str = "\u{1b}[3m";
79    pub const UNDERLINED: &'static str = "\u{1b}[4m";
80    pub const STRIKETHROUGH: &'static str = "\u{1b}[9m";
81    pub const OBFUSCATED: &'static str = "\u{1b}[8m";
82    pub const RESET: &'static str = "\u{1b}[m";
83
84    pub fn rgb(value: u32) -> String {
85        format!(
86            "\u{1b}[38;2;{};{};{}m",
87            (value >> 16) & 0xFF,
88            (value >> 8) & 0xFF,
89            value & 0xFF
90        )
91    }
92}
93
94#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
95#[cfg_attr(feature = "azalea-buf", derive(AzBuf))]
96pub enum ChatFormatting {
97    Black,
98    DarkBlue,
99    DarkGreen,
100    DarkAqua,
101    DarkRed,
102    DarkPurple,
103    Gold,
104    Gray,
105    DarkGray,
106    Blue,
107    Green,
108    Aqua,
109    Red,
110    LightPurple,
111    Yellow,
112    White,
113    Obfuscated,
114    Strikethrough,
115    Bold,
116    Underline,
117    Italic,
118    Reset,
119}
120
121impl ChatFormatting {
122    pub const FORMATTERS: [ChatFormatting; 22] = [
123        ChatFormatting::Black,
124        ChatFormatting::DarkBlue,
125        ChatFormatting::DarkGreen,
126        ChatFormatting::DarkAqua,
127        ChatFormatting::DarkRed,
128        ChatFormatting::DarkPurple,
129        ChatFormatting::Gold,
130        ChatFormatting::Gray,
131        ChatFormatting::DarkGray,
132        ChatFormatting::Blue,
133        ChatFormatting::Green,
134        ChatFormatting::Aqua,
135        ChatFormatting::Red,
136        ChatFormatting::LightPurple,
137        ChatFormatting::Yellow,
138        ChatFormatting::White,
139        ChatFormatting::Obfuscated,
140        ChatFormatting::Strikethrough,
141        ChatFormatting::Bold,
142        ChatFormatting::Underline,
143        ChatFormatting::Italic,
144        ChatFormatting::Reset,
145    ];
146
147    pub fn name(&self) -> &'static str {
148        match self {
149            ChatFormatting::Black => "BLACK",
150            ChatFormatting::DarkBlue => "DARK_BLUE",
151            ChatFormatting::DarkGreen => "DARK_GREEN",
152            ChatFormatting::DarkAqua => "DARK_AQUA",
153            ChatFormatting::DarkRed => "DARK_RED",
154            ChatFormatting::DarkPurple => "DARK_PURPLE",
155            ChatFormatting::Gold => "GOLD",
156            ChatFormatting::Gray => "GRAY",
157            ChatFormatting::DarkGray => "DARK_GRAY",
158            ChatFormatting::Blue => "BLUE",
159            ChatFormatting::Green => "GREEN",
160            ChatFormatting::Aqua => "AQUA",
161            ChatFormatting::Red => "RED",
162            ChatFormatting::LightPurple => "LIGHT_PURPLE",
163            ChatFormatting::Yellow => "YELLOW",
164            ChatFormatting::White => "WHITE",
165            ChatFormatting::Obfuscated => "OBFUSCATED",
166            ChatFormatting::Strikethrough => "STRIKETHROUGH",
167            ChatFormatting::Bold => "BOLD",
168            ChatFormatting::Underline => "UNDERLINE",
169            ChatFormatting::Italic => "ITALIC",
170            ChatFormatting::Reset => "RESET",
171        }
172    }
173
174    pub fn code(&self) -> char {
175        match self {
176            ChatFormatting::Black => '0',
177            ChatFormatting::DarkBlue => '1',
178            ChatFormatting::DarkGreen => '2',
179            ChatFormatting::DarkAqua => '3',
180            ChatFormatting::DarkRed => '4',
181            ChatFormatting::DarkPurple => '5',
182            ChatFormatting::Gold => '6',
183            ChatFormatting::Gray => '7',
184            ChatFormatting::DarkGray => '8',
185            ChatFormatting::Blue => '9',
186            ChatFormatting::Green => 'a',
187            ChatFormatting::Aqua => 'b',
188            ChatFormatting::Red => 'c',
189            ChatFormatting::LightPurple => 'd',
190            ChatFormatting::Yellow => 'e',
191            ChatFormatting::White => 'f',
192            ChatFormatting::Obfuscated => 'k',
193            ChatFormatting::Strikethrough => 'm',
194            ChatFormatting::Bold => 'l',
195            ChatFormatting::Underline => 'n',
196            ChatFormatting::Italic => 'o',
197            ChatFormatting::Reset => 'r',
198        }
199    }
200
201    pub fn from_code(code: char) -> Option<ChatFormatting> {
202        match code {
203            '0' => Some(ChatFormatting::Black),
204            '1' => Some(ChatFormatting::DarkBlue),
205            '2' => Some(ChatFormatting::DarkGreen),
206            '3' => Some(ChatFormatting::DarkAqua),
207            '4' => Some(ChatFormatting::DarkRed),
208            '5' => Some(ChatFormatting::DarkPurple),
209            '6' => Some(ChatFormatting::Gold),
210            '7' => Some(ChatFormatting::Gray),
211            '8' => Some(ChatFormatting::DarkGray),
212            '9' => Some(ChatFormatting::Blue),
213            'a' => Some(ChatFormatting::Green),
214            'b' => Some(ChatFormatting::Aqua),
215            'c' => Some(ChatFormatting::Red),
216            'd' => Some(ChatFormatting::LightPurple),
217            'e' => Some(ChatFormatting::Yellow),
218            'f' => Some(ChatFormatting::White),
219            'k' => Some(ChatFormatting::Obfuscated),
220            'm' => Some(ChatFormatting::Strikethrough),
221            'l' => Some(ChatFormatting::Bold),
222            'n' => Some(ChatFormatting::Underline),
223            'o' => Some(ChatFormatting::Italic),
224            'r' => Some(ChatFormatting::Reset),
225            _ => None,
226        }
227    }
228
229    pub fn is_format(&self) -> bool {
230        matches!(
231            self,
232            ChatFormatting::Obfuscated
233                | ChatFormatting::Strikethrough
234                | ChatFormatting::Bold
235                | ChatFormatting::Underline
236                | ChatFormatting::Italic
237                | ChatFormatting::Reset
238        )
239    }
240
241    pub fn color(&self) -> Option<u32> {
242        match self {
243            ChatFormatting::Black => Some(0),
244            ChatFormatting::DarkBlue => Some(170),
245            ChatFormatting::DarkGreen => Some(43520),
246            ChatFormatting::DarkAqua => Some(43690),
247            ChatFormatting::DarkRed => Some(1114112),
248            ChatFormatting::DarkPurple => Some(11141290),
249            ChatFormatting::Gold => Some(16755200),
250            ChatFormatting::Gray => Some(11184810),
251            ChatFormatting::DarkGray => Some(5592405),
252            ChatFormatting::Blue => Some(5592575),
253            ChatFormatting::Green => Some(5635925),
254            ChatFormatting::Aqua => Some(5636095),
255            ChatFormatting::Red => Some(16733525),
256            ChatFormatting::LightPurple => Some(16733695),
257            ChatFormatting::Yellow => Some(16777045),
258            ChatFormatting::White => Some(16777215),
259            _ => None,
260        }
261    }
262}
263
264impl TextColor {
265    fn new(value: u32, name: Option<String>) -> Self {
266        Self { value, name }
267    }
268
269    fn serialize(&self) -> String {
270        if let Some(name) = &self.name {
271            name.clone().to_ascii_lowercase()
272        } else {
273            self.format_value()
274        }
275    }
276
277    pub fn format_value(&self) -> String {
278        format!("#{:06X}", self.value)
279    }
280}
281
282impl fmt::Display for TextColor {
283    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
284        write!(f, "{}", self.serialize())
285    }
286}
287
288// from ChatFormatting to TextColor
289impl TryFrom<ChatFormatting> for TextColor {
290    type Error = String;
291
292    fn try_from(formatter: ChatFormatting) -> Result<Self, Self::Error> {
293        if formatter.is_format() {
294            return Err(format!("{} is not a color", formatter.name()));
295        }
296        let color = formatter.color().unwrap_or(0);
297        Ok(Self::new(color, Some(formatter.name().to_string())))
298    }
299}
300
301#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
302pub struct Style {
303    // These are options instead of just bools because None is different than false in this case
304    pub color: Option<TextColor>,
305    pub bold: Option<bool>,
306    pub italic: Option<bool>,
307    pub underlined: Option<bool>,
308    pub strikethrough: Option<bool>,
309    pub obfuscated: Option<bool>,
310    /// Whether formatting should be reset before applying these styles
311    pub reset: bool,
312}
313
314fn serde_serialize_field<S: serde::ser::SerializeStruct>(
315    state: &mut S,
316    name: &'static str,
317    value: &Option<impl serde::Serialize>,
318    default: &(impl serde::Serialize + ?Sized),
319    reset: bool,
320) -> Result<(), S::Error> {
321    if let Some(value) = value {
322        state.serialize_field(name, value)?;
323    } else if reset {
324        state.serialize_field(name, default)?;
325    }
326    Ok(())
327}
328
329#[cfg(feature = "simdnbt")]
330fn simdnbt_serialize_field(
331    compound: &mut simdnbt::owned::NbtCompound,
332    name: &'static str,
333    value: Option<impl simdnbt::ToNbtTag>,
334    default: impl simdnbt::ToNbtTag,
335    reset: bool,
336) {
337    match value {
338        Some(value) => {
339            compound.insert(name, value);
340        }
341        _ => {
342            if reset {
343                compound.insert(name, default);
344            }
345        }
346    }
347}
348
349impl Serialize for Style {
350    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
351    where
352        S: Serializer,
353    {
354        let len = if self.reset {
355            6
356        } else {
357            usize::from(self.color.is_some())
358                + usize::from(self.bold.is_some())
359                + usize::from(self.italic.is_some())
360                + usize::from(self.underlined.is_some())
361                + usize::from(self.strikethrough.is_some())
362                + usize::from(self.obfuscated.is_some())
363        };
364        let mut state = serializer.serialize_struct("Style", len)?;
365
366        serde_serialize_field(&mut state, "color", &self.color, "white", self.reset)?;
367        serde_serialize_field(&mut state, "bold", &self.bold, &false, self.reset)?;
368        serde_serialize_field(&mut state, "italic", &self.italic, &false, self.reset)?;
369        serde_serialize_field(
370            &mut state,
371            "underlined",
372            &self.underlined,
373            &false,
374            self.reset,
375        )?;
376        serde_serialize_field(
377            &mut state,
378            "strikethrough",
379            &self.strikethrough,
380            &false,
381            self.reset,
382        )?;
383        serde_serialize_field(
384            &mut state,
385            "obfuscated",
386            &self.obfuscated,
387            &false,
388            self.reset,
389        )?;
390
391        state.end()
392    }
393}
394
395#[cfg(feature = "simdnbt")]
396impl simdnbt::Serialize for Style {
397    fn to_compound(self) -> NbtCompound {
398        let mut compound = NbtCompound::new();
399
400        simdnbt_serialize_field(&mut compound, "color", self.color, "white", self.reset);
401        simdnbt_serialize_field(&mut compound, "bold", self.bold, false, self.reset);
402        simdnbt_serialize_field(&mut compound, "italic", self.italic, false, self.reset);
403        simdnbt_serialize_field(
404            &mut compound,
405            "underlined",
406            self.underlined,
407            false,
408            self.reset,
409        );
410        simdnbt_serialize_field(
411            &mut compound,
412            "strikethrough",
413            self.strikethrough,
414            false,
415            self.reset,
416        );
417        simdnbt_serialize_field(
418            &mut compound,
419            "obfuscated",
420            self.obfuscated,
421            false,
422            self.reset,
423        );
424
425        compound
426    }
427}
428
429impl Style {
430    pub fn empty() -> Self {
431        Self::default()
432    }
433
434    pub fn deserialize(json: &Value) -> Style {
435        let Some(json_object) = json.as_object() else {
436            return Style::default();
437        };
438        let bold = json_object.get("bold").and_then(|v| v.as_bool());
439        let italic = json_object.get("italic").and_then(|v| v.as_bool());
440        let underlined = json_object.get("underlined").and_then(|v| v.as_bool());
441        let strikethrough = json_object.get("strikethrough").and_then(|v| v.as_bool());
442        let obfuscated = json_object.get("obfuscated").and_then(|v| v.as_bool());
443        let color: Option<TextColor> = json_object
444            .get("color")
445            .and_then(|v| v.as_str())
446            .and_then(|v| TextColor::parse(v.to_string()));
447        Style {
448            color,
449            bold,
450            italic,
451            underlined,
452            strikethrough,
453            obfuscated,
454            ..Style::default()
455        }
456    }
457
458    /// Check if a style has no attributes set
459    pub fn is_empty(&self) -> bool {
460        self.color.is_none()
461            && self.bold.is_none()
462            && self.italic.is_none()
463            && self.underlined.is_none()
464            && self.strikethrough.is_none()
465            && self.obfuscated.is_none()
466    }
467
468    /// find the necessary ansi code to get from this style to another
469    pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String {
470        let should_reset = after.reset ||
471            // if it used to be bold and now it's not, reset
472            (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) ||
473            // if it used to be italic and now it's not, reset
474            (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) ||
475            // if it used to be underlined and now it's not, reset
476            (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) ||
477            // if it used to be strikethrough and now it's not, reset
478            (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) ||
479            // if it used to be obfuscated and now it's not, reset
480            (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true));
481
482        let mut ansi_codes = String::new();
483
484        let empty_style = Style::empty();
485
486        let (before, after) = if should_reset {
487            ansi_codes.push_str(Ansi::RESET);
488            let mut updated_after = if after.reset {
489                default_style.clone()
490            } else {
491                self.clone()
492            };
493            updated_after.apply(after);
494            (&empty_style, updated_after)
495        } else {
496            (self, after.clone())
497        };
498
499        // if bold used to be false/default and now it's true, set bold
500        if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) {
501            ansi_codes.push_str(Ansi::BOLD);
502        }
503        // if italic used to be false/default and now it's true, set italic
504        if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) {
505            ansi_codes.push_str(Ansi::ITALIC);
506        }
507        // if underlined used to be false/default and now it's true, set underlined
508        if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) {
509            ansi_codes.push_str(Ansi::UNDERLINED);
510        }
511        // if strikethrough used to be false/default and now it's true, set
512        // strikethrough
513        if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) {
514            ansi_codes.push_str(Ansi::STRIKETHROUGH);
515        }
516        // if obfuscated used to be false/default and now it's true, set obfuscated
517        if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) {
518            ansi_codes.push_str(Ansi::OBFUSCATED);
519        }
520
521        // if the new color is different and not none, set color
522        let color_changed = {
523            if before.color.is_none() && after.color.is_some() {
524                true
525            } else if before.color.is_some() && after.color.is_some() {
526                before.color.clone().unwrap().value != after.color.as_ref().unwrap().value
527            } else {
528                false
529            }
530        };
531
532        if color_changed {
533            let after_color = after.color.as_ref().unwrap();
534            ansi_codes.push_str(&Ansi::rgb(after_color.value));
535        }
536
537        ansi_codes
538    }
539
540    /// Apply another style to this one
541    pub fn apply(&mut self, style: &Style) {
542        if let Some(color) = &style.color {
543            self.color = Some(color.clone());
544        }
545        if let Some(bold) = &style.bold {
546            self.bold = Some(*bold);
547        }
548        if let Some(italic) = &style.italic {
549            self.italic = Some(*italic);
550        }
551        if let Some(underlined) = &style.underlined {
552            self.underlined = Some(*underlined);
553        }
554        if let Some(strikethrough) = &style.strikethrough {
555            self.strikethrough = Some(*strikethrough);
556        }
557        if let Some(obfuscated) = &style.obfuscated {
558            self.obfuscated = Some(*obfuscated);
559        }
560    }
561
562    /// Returns a new style that is a merge of self and other.
563    /// For any field that `other` does not specify (is None), self’s value is
564    /// used.
565    pub fn merged_with(&self, other: &Style) -> Style {
566        Style {
567            color: other.color.clone().or(self.color.clone()),
568            bold: other.bold.or(self.bold),
569            italic: other.italic.or(self.italic),
570            underlined: other.underlined.or(self.underlined),
571            strikethrough: other.strikethrough.or(self.strikethrough),
572            obfuscated: other.obfuscated.or(self.obfuscated),
573            reset: other.reset, // if reset is true in the new style, that takes precedence
574        }
575    }
576
577    /// Apply a ChatFormatting to this style
578    pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
579        match *formatting {
580            ChatFormatting::Bold => self.bold = Some(true),
581            ChatFormatting::Italic => self.italic = Some(true),
582            ChatFormatting::Underline => self.underlined = Some(true),
583            ChatFormatting::Strikethrough => self.strikethrough = Some(true),
584            ChatFormatting::Obfuscated => self.obfuscated = Some(true),
585            ChatFormatting::Reset => self.reset = true,
586            formatter => {
587                // if it's a color, set it
588                if let Some(color) = formatter.color() {
589                    self.color = Some(TextColor::from_rgb(color));
590                }
591            }
592        }
593    }
594
595    pub fn get_html_style(&self) -> String {
596        let mut style = String::new();
597        if let Some(color) = &self.color {
598            style.push_str(&format!("color:{};", color.format_value()));
599        }
600        if let Some(bold) = self.bold {
601            style.push_str(&format!(
602                "font-weight:{};",
603                if bold { "bold" } else { "normal" }
604            ));
605        }
606        if let Some(italic) = self.italic {
607            style.push_str(&format!(
608                "font-style:{};",
609                if italic { "italic" } else { "normal" }
610            ));
611        }
612        if let Some(underlined) = self.underlined {
613            style.push_str(&format!(
614                "text-decoration:{};",
615                if underlined { "underline" } else { "none" }
616            ));
617        }
618        if let Some(strikethrough) = self.strikethrough {
619            style.push_str(&format!(
620                "text-decoration:{};",
621                if strikethrough {
622                    "line-through"
623                } else {
624                    "none"
625                }
626            ));
627        }
628        if let Some(obfuscated) = self.obfuscated
629            && obfuscated
630        {
631            style.push_str("filter:blur(2px);");
632        }
633
634        style
635    }
636}
637
638#[cfg(feature = "simdnbt")]
639impl simdnbt::Deserialize for Style {
640    fn from_compound(
641        compound: simdnbt::borrow::NbtCompound,
642    ) -> Result<Self, simdnbt::DeserializeError> {
643        let bold = compound.byte("bold").map(|v| v != 0);
644        let italic = compound.byte("italic").map(|v| v != 0);
645        let underlined = compound.byte("underlined").map(|v| v != 0);
646        let strikethrough = compound.byte("strikethrough").map(|v| v != 0);
647        let obfuscated = compound.byte("obfuscated").map(|v| v != 0);
648        let color: Option<TextColor> = compound
649            .string("color")
650            .and_then(|v| TextColor::parse(v.to_string()));
651        Ok(Style {
652            color,
653            bold,
654            italic,
655            underlined,
656            strikethrough,
657            obfuscated,
658            ..Style::default()
659        })
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666    use crate::component::DEFAULT_STYLE;
667
668    #[test]
669    fn text_color_named_colors() {
670        assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525);
671    }
672    #[test]
673    fn text_color_hex_colors() {
674        assert_eq!(
675            TextColor::parse("#a1b2c3".to_string()).unwrap().value,
676            10597059
677        );
678    }
679
680    #[test]
681    fn ansi_difference_should_reset() {
682        let style_a = Style {
683            bold: Some(true),
684            italic: Some(true),
685            ..Style::default()
686        };
687        let style_b = Style {
688            bold: Some(false),
689            ..Style::default()
690        };
691        let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
692        assert_eq!(
693            ansi_difference,
694            format!(
695                "{reset}{italic}",
696                reset = Ansi::RESET,
697                italic = Ansi::ITALIC
698            )
699        )
700    }
701    #[test]
702    fn ansi_difference_shouldnt_reset() {
703        let style_a = Style {
704            bold: Some(true),
705            ..Style::default()
706        };
707        let style_b = Style {
708            italic: Some(true),
709            ..Style::default()
710        };
711        let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
712        assert_eq!(ansi_difference, Ansi::ITALIC)
713    }
714
715    #[test]
716    fn ansi_difference_explicit_reset() {
717        let style_a = Style {
718            bold: Some(true),
719            ..Style::empty()
720        };
721        let style_b = Style {
722            italic: Some(true),
723            reset: true,
724            ..Style::empty()
725        };
726        let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE);
727        assert_eq!(
728            ansi_difference,
729            format!(
730                "{reset}{italic}{white}",
731                reset = Ansi::RESET,
732                white = Ansi::rgb(ChatFormatting::White.color().unwrap()),
733                italic = Ansi::ITALIC
734            )
735        )
736    }
737
738    #[test]
739    fn test_from_code() {
740        assert_eq!(
741            ChatFormatting::from_code('a').unwrap(),
742            ChatFormatting::Green
743        );
744    }
745
746    #[test]
747    fn test_apply_formatting() {
748        let mut style = Style::default();
749        style.apply_formatting(&ChatFormatting::Bold);
750        style.apply_formatting(&ChatFormatting::Red);
751        assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
752    }
753}