1use std::{collections::HashMap, fmt, sync::LazyLock};
2
3#[cfg(feature = "azalea-buf")]
4use azalea_buf::AzBuf;
5use serde::{ser::SerializeStruct, Serialize, Serializer};
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()
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
288impl 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 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 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 if let Some(value) = value {
338 compound.insert(name, value);
339 } else if reset {
340 compound.insert(name, default);
341 }
342}
343
344impl Serialize for Style {
345 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
346 where
347 S: Serializer,
348 {
349 let len = if self.reset {
350 6
351 } else {
352 usize::from(self.color.is_some())
353 + usize::from(self.bold.is_some())
354 + usize::from(self.italic.is_some())
355 + usize::from(self.underlined.is_some())
356 + usize::from(self.strikethrough.is_some())
357 + usize::from(self.obfuscated.is_some())
358 };
359 let mut state = serializer.serialize_struct("Style", len)?;
360
361 serde_serialize_field(&mut state, "color", &self.color, "white", self.reset)?;
362 serde_serialize_field(&mut state, "bold", &self.bold, &false, self.reset)?;
363 serde_serialize_field(&mut state, "italic", &self.italic, &false, self.reset)?;
364 serde_serialize_field(
365 &mut state,
366 "underlined",
367 &self.underlined,
368 &false,
369 self.reset,
370 )?;
371 serde_serialize_field(
372 &mut state,
373 "strikethrough",
374 &self.strikethrough,
375 &false,
376 self.reset,
377 )?;
378 serde_serialize_field(
379 &mut state,
380 "obfuscated",
381 &self.obfuscated,
382 &false,
383 self.reset,
384 )?;
385
386 state.end()
387 }
388}
389
390#[cfg(feature = "simdnbt")]
391impl simdnbt::Serialize for Style {
392 fn to_compound(self) -> NbtCompound {
393 let mut compound = NbtCompound::new();
394
395 simdnbt_serialize_field(&mut compound, "color", self.color, "white", self.reset);
396 simdnbt_serialize_field(&mut compound, "bold", self.bold, false, self.reset);
397 simdnbt_serialize_field(&mut compound, "italic", self.italic, false, self.reset);
398 simdnbt_serialize_field(
399 &mut compound,
400 "underlined",
401 self.underlined,
402 false,
403 self.reset,
404 );
405 simdnbt_serialize_field(
406 &mut compound,
407 "strikethrough",
408 self.strikethrough,
409 false,
410 self.reset,
411 );
412 simdnbt_serialize_field(
413 &mut compound,
414 "obfuscated",
415 self.obfuscated,
416 false,
417 self.reset,
418 );
419
420 compound
421 }
422}
423
424impl Style {
425 pub fn empty() -> Self {
426 Self::default()
427 }
428
429 pub fn deserialize(json: &Value) -> Style {
430 let Some(json_object) = json.as_object() else {
431 return Style::default();
432 };
433 let bold = json_object.get("bold").and_then(|v| v.as_bool());
434 let italic = json_object.get("italic").and_then(|v| v.as_bool());
435 let underlined = json_object.get("underlined").and_then(|v| v.as_bool());
436 let strikethrough = json_object.get("strikethrough").and_then(|v| v.as_bool());
437 let obfuscated = json_object.get("obfuscated").and_then(|v| v.as_bool());
438 let color: Option<TextColor> = json_object
439 .get("color")
440 .and_then(|v| v.as_str())
441 .and_then(|v| TextColor::parse(v.to_string()));
442 Style {
443 color,
444 bold,
445 italic,
446 underlined,
447 strikethrough,
448 obfuscated,
449 ..Style::default()
450 }
451 }
452
453 pub fn is_empty(&self) -> bool {
455 self.color.is_none()
456 && self.bold.is_none()
457 && self.italic.is_none()
458 && self.underlined.is_none()
459 && self.strikethrough.is_none()
460 && self.obfuscated.is_none()
461 }
462
463 pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String {
465 let should_reset = after.reset ||
466 (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) ||
468 (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) ||
470 (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) ||
472 (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) ||
474 (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true));
476
477 let mut ansi_codes = String::new();
478
479 let empty_style = Style::empty();
480
481 let (before, after) = if should_reset {
482 ansi_codes.push_str(Ansi::RESET);
483 let mut updated_after = if after.reset {
484 default_style.clone()
485 } else {
486 self.clone()
487 };
488 updated_after.apply(after);
489 (&empty_style, updated_after)
490 } else {
491 (self, after.clone())
492 };
493
494 if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) {
496 ansi_codes.push_str(Ansi::BOLD);
497 }
498 if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) {
500 ansi_codes.push_str(Ansi::ITALIC);
501 }
502 if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) {
504 ansi_codes.push_str(Ansi::UNDERLINED);
505 }
506 if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) {
509 ansi_codes.push_str(Ansi::STRIKETHROUGH);
510 }
511 if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) {
513 ansi_codes.push_str(Ansi::OBFUSCATED);
514 }
515
516 let color_changed = {
518 if before.color.is_none() && after.color.is_some() {
519 true
520 } else if before.color.is_some() && after.color.is_some() {
521 before.color.clone().unwrap().value != after.color.as_ref().unwrap().value
522 } else {
523 false
524 }
525 };
526
527 if color_changed {
528 let after_color = after.color.as_ref().unwrap();
529 ansi_codes.push_str(&Ansi::rgb(after_color.value));
530 }
531
532 ansi_codes
533 }
534
535 pub fn apply(&mut self, style: &Style) {
537 if let Some(color) = &style.color {
538 self.color = Some(color.clone());
539 }
540 if let Some(bold) = &style.bold {
541 self.bold = Some(*bold);
542 }
543 if let Some(italic) = &style.italic {
544 self.italic = Some(*italic);
545 }
546 if let Some(underlined) = &style.underlined {
547 self.underlined = Some(*underlined);
548 }
549 if let Some(strikethrough) = &style.strikethrough {
550 self.strikethrough = Some(*strikethrough);
551 }
552 if let Some(obfuscated) = &style.obfuscated {
553 self.obfuscated = Some(*obfuscated);
554 }
555 }
556
557 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 => self.reset = true,
566 formatter => {
567 if let Some(color) = formatter.color() {
569 self.color = Some(TextColor::from_rgb(color));
570 }
571 }
572 }
573 }
574}
575
576#[cfg(feature = "simdnbt")]
577impl simdnbt::Deserialize for Style {
578 fn from_compound(
579 compound: simdnbt::borrow::NbtCompound,
580 ) -> Result<Self, simdnbt::DeserializeError> {
581 let bold = compound.byte("bold").map(|v| v != 0);
582 let italic = compound.byte("italic").map(|v| v != 0);
583 let underlined = compound.byte("underlined").map(|v| v != 0);
584 let strikethrough = compound.byte("strikethrough").map(|v| v != 0);
585 let obfuscated = compound.byte("obfuscated").map(|v| v != 0);
586 let color: Option<TextColor> = compound
587 .string("color")
588 .and_then(|v| TextColor::parse(v.to_string()));
589 Ok(Style {
590 color,
591 bold,
592 italic,
593 underlined,
594 strikethrough,
595 obfuscated,
596 ..Style::default()
597 })
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::component::DEFAULT_STYLE;
605
606 #[test]
607 fn text_color_named_colors() {
608 assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525);
609 }
610 #[test]
611 fn text_color_hex_colors() {
612 assert_eq!(
613 TextColor::parse("#a1b2c3".to_string()).unwrap().value,
614 10597059
615 );
616 }
617
618 #[test]
619 fn ansi_difference_should_reset() {
620 let style_a = Style {
621 bold: Some(true),
622 italic: Some(true),
623 ..Style::default()
624 };
625 let style_b = Style {
626 bold: Some(false),
627 ..Style::default()
628 };
629 let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
630 assert_eq!(
631 ansi_difference,
632 format!(
633 "{reset}{italic}",
634 reset = Ansi::RESET,
635 italic = Ansi::ITALIC
636 )
637 )
638 }
639 #[test]
640 fn ansi_difference_shouldnt_reset() {
641 let style_a = Style {
642 bold: Some(true),
643 ..Style::default()
644 };
645 let style_b = Style {
646 italic: Some(true),
647 ..Style::default()
648 };
649 let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
650 assert_eq!(ansi_difference, Ansi::ITALIC)
651 }
652
653 #[test]
654 fn ansi_difference_explicit_reset() {
655 let style_a = Style {
656 bold: Some(true),
657 ..Style::empty()
658 };
659 let style_b = Style {
660 italic: Some(true),
661 reset: true,
662 ..Style::empty()
663 };
664 let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE);
665 assert_eq!(
666 ansi_difference,
667 format!(
668 "{reset}{italic}{white}",
669 reset = Ansi::RESET,
670 white = Ansi::rgb(ChatFormatting::White.color().unwrap()),
671 italic = Ansi::ITALIC
672 )
673 )
674 }
675
676 #[test]
677 fn test_from_code() {
678 assert_eq!(
679 ChatFormatting::from_code('a').unwrap(),
680 ChatFormatting::Green
681 );
682 }
683
684 #[test]
685 fn test_apply_formatting() {
686 let mut style = Style::default();
687 style.apply_formatting(&ChatFormatting::Bold);
688 style.apply_formatting(&ChatFormatting::Red);
689 assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
690 }
691}