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