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()
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 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 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 pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String {
470 let should_reset = after.reset ||
471 (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) ||
473 (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) ||
475 (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) ||
477 (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) ||
479 (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 !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) {
501 ansi_codes.push_str(Ansi::BOLD);
502 }
503 if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) {
505 ansi_codes.push_str(Ansi::ITALIC);
506 }
507 if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) {
509 ansi_codes.push_str(Ansi::UNDERLINED);
510 }
511 if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) {
514 ansi_codes.push_str(Ansi::STRIKETHROUGH);
515 }
516 if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) {
518 ansi_codes.push_str(Ansi::OBFUSCATED);
519 }
520
521 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 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 pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
564 match *formatting {
565 ChatFormatting::Bold => self.bold = Some(true),
566 ChatFormatting::Italic => self.italic = Some(true),
567 ChatFormatting::Underline => self.underlined = Some(true),
568 ChatFormatting::Strikethrough => self.strikethrough = Some(true),
569 ChatFormatting::Obfuscated => self.obfuscated = Some(true),
570 ChatFormatting::Reset => self.reset = true,
571 formatter => {
572 if let Some(color) = formatter.color() {
574 self.color = Some(TextColor::from_rgb(color));
575 }
576 }
577 }
578 }
579}
580
581#[cfg(feature = "simdnbt")]
582impl simdnbt::Deserialize for Style {
583 fn from_compound(
584 compound: simdnbt::borrow::NbtCompound,
585 ) -> Result<Self, simdnbt::DeserializeError> {
586 let bold = compound.byte("bold").map(|v| v != 0);
587 let italic = compound.byte("italic").map(|v| v != 0);
588 let underlined = compound.byte("underlined").map(|v| v != 0);
589 let strikethrough = compound.byte("strikethrough").map(|v| v != 0);
590 let obfuscated = compound.byte("obfuscated").map(|v| v != 0);
591 let color: Option<TextColor> = compound
592 .string("color")
593 .and_then(|v| TextColor::parse(v.to_string()));
594 Ok(Style {
595 color,
596 bold,
597 italic,
598 underlined,
599 strikethrough,
600 obfuscated,
601 ..Style::default()
602 })
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use crate::component::DEFAULT_STYLE;
610
611 #[test]
612 fn text_color_named_colors() {
613 assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525);
614 }
615 #[test]
616 fn text_color_hex_colors() {
617 assert_eq!(
618 TextColor::parse("#a1b2c3".to_string()).unwrap().value,
619 10597059
620 );
621 }
622
623 #[test]
624 fn ansi_difference_should_reset() {
625 let style_a = Style {
626 bold: Some(true),
627 italic: Some(true),
628 ..Style::default()
629 };
630 let style_b = Style {
631 bold: Some(false),
632 ..Style::default()
633 };
634 let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
635 assert_eq!(
636 ansi_difference,
637 format!(
638 "{reset}{italic}",
639 reset = Ansi::RESET,
640 italic = Ansi::ITALIC
641 )
642 )
643 }
644 #[test]
645 fn ansi_difference_shouldnt_reset() {
646 let style_a = Style {
647 bold: Some(true),
648 ..Style::default()
649 };
650 let style_b = Style {
651 italic: Some(true),
652 ..Style::default()
653 };
654 let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
655 assert_eq!(ansi_difference, Ansi::ITALIC)
656 }
657
658 #[test]
659 fn ansi_difference_explicit_reset() {
660 let style_a = Style {
661 bold: Some(true),
662 ..Style::empty()
663 };
664 let style_b = Style {
665 italic: Some(true),
666 reset: true,
667 ..Style::empty()
668 };
669 let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE);
670 assert_eq!(
671 ansi_difference,
672 format!(
673 "{reset}{italic}{white}",
674 reset = Ansi::RESET,
675 white = Ansi::rgb(ChatFormatting::White.color().unwrap()),
676 italic = Ansi::ITALIC
677 )
678 )
679 }
680
681 #[test]
682 fn test_from_code() {
683 assert_eq!(
684 ChatFormatting::from_code('a').unwrap(),
685 ChatFormatting::Green
686 );
687 }
688
689 #[test]
690 fn test_apply_formatting() {
691 let mut style = Style::default();
692 style.apply_formatting(&ChatFormatting::Bold);
693 style.apply_formatting(&ChatFormatting::Red);
694 assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
695 }
696}