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
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 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, }
575 }
576
577 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 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}