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