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 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 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
294impl 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 #[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 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 pub fn compare_ansi(&self, after: &Style) -> String {
452 let should_reset =
453 (self.bold.unwrap_or_default() && !after.bold.unwrap_or_default()) ||
455 (self.italic.unwrap_or_default() && !after.italic.unwrap_or_default()) ||
457 (self.underlined.unwrap_or_default() && !after.underlined.unwrap_or_default()) ||
459 (self.strikethrough.unwrap_or_default() && !after.strikethrough.unwrap_or_default()) ||
461 (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 !before.bold.unwrap_or_default() && after.bold.unwrap_or_default() {
477 ansi_codes.push_str(Ansi::BOLD);
478 }
479 if !before.italic.unwrap_or_default() && after.italic.unwrap_or_default() {
481 ansi_codes.push_str(Ansi::ITALIC);
482 }
483 if !before.underlined.unwrap_or_default() && after.underlined.unwrap_or_default() {
485 ansi_codes.push_str(Ansi::UNDERLINED);
486 }
487 if !before.strikethrough.unwrap_or_default() && after.strikethrough.unwrap_or_default() {
490 ansi_codes.push_str(Ansi::STRIKETHROUGH);
491 }
492 if !before.obfuscated.unwrap_or_default() && after.obfuscated.unwrap_or_default() {
494 ansi_codes.push_str(Ansi::OBFUSCATED);
495 }
496
497 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 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 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 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 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}