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 for formatter in &Self::FORMATTERS {
205 if formatter.name() == name {
206 return Some(formatter);
207 }
208 }
209 None
210 }
211
212 pub fn code(&self) -> char {
213 match self {
214 Self::Black => '0',
215 Self::DarkBlue => '1',
216 Self::DarkGreen => '2',
217 Self::DarkAqua => '3',
218 Self::DarkRed => '4',
219 Self::DarkPurple => '5',
220 Self::Gold => '6',
221 Self::Gray => '7',
222 Self::DarkGray => '8',
223 Self::Blue => '9',
224 Self::Green => 'a',
225 Self::Aqua => 'b',
226 Self::Red => 'c',
227 Self::LightPurple => 'd',
228 Self::Yellow => 'e',
229 Self::White => 'f',
230 Self::Obfuscated => 'k',
231 Self::Strikethrough => 'm',
232 Self::Bold => 'l',
233 Self::Underline => 'n',
234 Self::Italic => 'o',
235 Self::Reset => 'r',
236 }
237 }
238
239 pub fn from_code(code: char) -> Option<Self> {
240 Some(match code {
241 '0' => Self::Black,
242 '1' => Self::DarkBlue,
243 '2' => Self::DarkGreen,
244 '3' => Self::DarkAqua,
245 '4' => Self::DarkRed,
246 '5' => Self::DarkPurple,
247 '6' => Self::Gold,
248 '7' => Self::Gray,
249 '8' => Self::DarkGray,
250 '9' => Self::Blue,
251 'a' => Self::Green,
252 'b' => Self::Aqua,
253 'c' => Self::Red,
254 'd' => Self::LightPurple,
255 'e' => Self::Yellow,
256 'f' => Self::White,
257 'k' => Self::Obfuscated,
258 'm' => Self::Strikethrough,
259 'l' => Self::Bold,
260 'n' => Self::Underline,
261 'o' => Self::Italic,
262 'r' => Self::Reset,
263 _ => return None,
264 })
265 }
266
267 pub fn is_format(&self) -> bool {
268 matches!(
269 self,
270 Self::Obfuscated
271 | Self::Strikethrough
272 | Self::Bold
273 | Self::Underline
274 | Self::Italic
275 | Self::Reset
276 )
277 }
278
279 pub fn color(&self) -> Option<u32> {
280 Some(match self {
281 Self::Black => 0,
282 Self::DarkBlue => 170,
283 Self::DarkGreen => 43520,
284 Self::DarkAqua => 43690,
285 Self::DarkRed => 11141120,
286 Self::DarkPurple => 11141290,
287 Self::Gold => 16755200,
288 Self::Gray => 11184810,
289 Self::DarkGray => 5592405,
290 Self::Blue => 5592575,
291 Self::Green => 5635925,
292 Self::Aqua => 5636095,
293 Self::Red => 16733525,
294 Self::LightPurple => 16733695,
295 Self::Yellow => 16777045,
296 Self::White => 16777215,
297 _ => return None,
298 })
299 }
300}
301
302impl TryFrom<ChatFormatting> for TextColor {
304 type Error = String;
305
306 fn try_from(formatter: ChatFormatting) -> Result<Self, Self::Error> {
307 if formatter.is_format() {
308 return Err(format!("{} is not a color", formatter.name()));
309 }
310 let color = formatter.color().unwrap_or(0);
311 Ok(Self::new(color, Some(formatter.name().to_owned())))
312 }
313}
314
315macro_rules! define_style_struct {
316 ($($(#[$doc:meta])* $field:ident : $type:ty),* $(,)?) => {
317 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
318 #[non_exhaustive]
319 pub struct Style {
320 $(
321 #[serde(skip_serializing_if = "Option::is_none")]
322 $(#[$doc])*
323 pub $field: Option<$type>,
324 )*
325 }
326
327 impl Style {
328 $(
329 pub fn $field(mut self, value: impl Into<Option<$type>>) -> Self {
330 self.$field = value.into();
331 self
332 }
333 )*
334
335 pub fn serialize_map<S>(&self, state: &mut S::SerializeMap) -> Result<(), S::Error>
336 where
337 S: serde::Serializer,
338 {
339 $(
340 if let Some(value) = &self.$field {
341 state.serialize_entry(stringify!($field), value)?;
342 }
343 )*
344 Ok(())
345 }
346
347 pub fn apply(&mut self, style: &Style) {
349 $(
350 if let Some(value) = &style.$field {
351 self.$field = Some(value.clone());
352 }
353 )*
354 }
355 }
356
357 #[cfg(feature = "simdnbt")]
358 impl simdnbt::Serialize for Style {
359 fn to_compound(self) -> NbtCompound {
360 let mut compound = NbtCompound::new();
361
362 $(
363 if let Some(value) = self.$field {
364 compound.insert(stringify!($field), value);
365 }
366 )*
367
368 compound
369 }
370 }
371 };
372}
373
374define_style_struct! {
375 color: TextColor,
376 shadow_color: u32,
377 bold: bool,
378 italic: bool,
379 underlined: bool,
380 strikethrough: bool,
381 obfuscated: bool,
382 click_event: ClickEvent,
383 hover_event: HoverEvent,
384 insertion: String,
385 font: String,
387}
388
389impl Style {
390 pub fn new() -> Self {
391 Self::default()
392 }
393
394 pub fn empty() -> Self {
395 Self::default()
396 }
397
398 pub fn deserialize(json: &Value) -> Style {
399 let Some(j) = json.as_object() else {
400 return Style::default();
401 };
402
403 Style {
404 color: j
405 .get("color")
406 .and_then(|v| v.as_str())
407 .and_then(TextColor::parse),
408 shadow_color: j
409 .get("shadow_color")
410 .and_then(|v| v.as_u64())
411 .map(|v| v as u32),
412 bold: j.get("bold").and_then(|v| v.as_bool()),
413 italic: j.get("italic").and_then(|v| v.as_bool()),
414 underlined: j.get("underlined").and_then(|v| v.as_bool()),
415 strikethrough: j.get("strikethrough").and_then(|v| v.as_bool()),
416 obfuscated: j.get("obfuscated").and_then(|v| v.as_bool()),
417 click_event: Default::default(),
419 hover_event: Default::default(),
420 insertion: j
421 .get("insertion")
422 .and_then(|v| v.as_str())
423 .map(|s| s.to_owned()),
424 font: j.get("font").and_then(|v| v.as_str()).map(|s| s.to_owned()),
425 }
426 }
427
428 pub fn is_empty(&self) -> bool {
430 self.color.is_none()
431 && self.bold.is_none()
432 && self.italic.is_none()
433 && self.underlined.is_none()
434 && self.strikethrough.is_none()
435 && self.obfuscated.is_none()
436 }
437
438 pub fn compare_ansi(&self, after: &Style) -> String {
440 let should_reset =
441 (self.bold.unwrap_or_default() && !after.bold.unwrap_or_default()) ||
443 (self.italic.unwrap_or_default() && !after.italic.unwrap_or_default()) ||
444 (self.underlined.unwrap_or_default() && !after.underlined.unwrap_or_default()) ||
445 (self.strikethrough.unwrap_or_default() && !after.strikethrough.unwrap_or_default()) ||
446 (self.obfuscated.unwrap_or_default() && !after.obfuscated.unwrap_or_default());
447
448 let mut ansi_codes = String::new();
449
450 let empty_style = Style::empty();
451
452 let before = if should_reset {
453 ansi_codes.push_str(Ansi::RESET);
454 &empty_style
455 } else {
456 self
457 };
458
459 if !before.bold.unwrap_or_default() && after.bold.unwrap_or_default() {
461 ansi_codes.push_str(Ansi::BOLD);
462 }
463 if !before.italic.unwrap_or_default() && after.italic.unwrap_or_default() {
464 ansi_codes.push_str(Ansi::ITALIC);
465 }
466 if !before.underlined.unwrap_or_default() && after.underlined.unwrap_or_default() {
467 ansi_codes.push_str(Ansi::UNDERLINED);
468 }
469 if !before.strikethrough.unwrap_or_default() && after.strikethrough.unwrap_or_default() {
470 ansi_codes.push_str(Ansi::STRIKETHROUGH);
471 }
472 if !before.obfuscated.unwrap_or_default() && after.obfuscated.unwrap_or_default() {
473 ansi_codes.push_str(Ansi::OBFUSCATED);
474 }
475
476 let color_changed = {
478 if before.color.is_none() && after.color.is_some() {
479 true
480 } else if let Some(before_color) = &before.color
481 && let Some(after_color) = &after.color
482 {
483 before_color.value != after_color.value
484 } else {
485 false
486 }
487 };
488
489 if color_changed {
490 let after_color = after.color.as_ref().unwrap();
491 ansi_codes.push_str(&Ansi::rgb(after_color.value));
492 }
493
494 ansi_codes
495 }
496
497 pub fn merged_with(&self, other: &Style) -> Style {
501 Style {
502 color: other.color.clone().or(self.color.clone()),
503 shadow_color: other.shadow_color.or(self.shadow_color),
504 bold: other.bold.or(self.bold),
505 italic: other.italic.or(self.italic),
506 underlined: other.underlined.or(self.underlined),
507 strikethrough: other.strikethrough.or(self.strikethrough),
508 obfuscated: other.obfuscated.or(self.obfuscated),
509 click_event: other.click_event.clone().or(self.click_event.clone()),
510 hover_event: other.hover_event.clone().or(self.hover_event.clone()),
511 insertion: other.insertion.clone().or(self.insertion.clone()),
512 font: other.font.clone().or(self.font.clone()),
513 }
514 }
515
516 pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
518 match *formatting {
519 ChatFormatting::Bold => self.bold = Some(true),
520 ChatFormatting::Italic => self.italic = Some(true),
521 ChatFormatting::Underline => self.underlined = Some(true),
522 ChatFormatting::Strikethrough => self.strikethrough = Some(true),
523 ChatFormatting::Obfuscated => self.obfuscated = Some(true),
524 ChatFormatting::Reset => {
525 self.color = None;
526 self.bold = None;
527 self.italic = None;
528 self.underlined = None;
529 self.strikethrough = None;
530 self.obfuscated = None;
531 }
532 formatter => {
533 if let Some(color) = formatter.color() {
535 self.color = Some(TextColor::from_rgb(color));
536 }
537 }
538 }
539 }
540
541 pub fn get_html_style(&self) -> String {
542 let mut style = String::new();
543 if let Some(color) = &self.color {
544 style.push_str(&format!("color:{};", color.format_value()));
545 }
546 if let Some(bold) = self.bold {
547 style.push_str(&format!(
548 "font-weight:{};",
549 if bold { "bold" } else { "normal" }
550 ));
551 }
552 if let Some(italic) = self.italic {
553 style.push_str(&format!(
554 "font-style:{};",
555 if italic { "italic" } else { "normal" }
556 ));
557 }
558 if let Some(underlined) = self.underlined {
559 style.push_str(&format!(
560 "text-decoration:{};",
561 if underlined { "underline" } else { "none" }
562 ));
563 }
564 if let Some(strikethrough) = self.strikethrough {
565 style.push_str(&format!(
566 "text-decoration:{};",
567 if strikethrough {
568 "line-through"
569 } else {
570 "none"
571 }
572 ));
573 }
574 if let Some(obfuscated) = self.obfuscated
575 && obfuscated
576 {
577 style.push_str("filter:blur(2px);");
578 }
579
580 style
581 }
582}
583
584#[cfg(feature = "simdnbt")]
585impl simdnbt::Deserialize for Style {
586 fn from_compound(
587 compound: simdnbt::borrow::NbtCompound,
588 ) -> Result<Self, simdnbt::DeserializeError> {
589 use crate::get_in_compound;
590
591 let color: Option<TextColor> = compound
592 .string("color")
593 .and_then(|v| TextColor::parse(&v.to_str()));
594 let shadow_color = get_in_compound(&compound, "shadow_color").ok();
595 let bold = get_in_compound(&compound, "bold").ok();
596 let italic = get_in_compound(&compound, "italic").ok();
597 let underlined = get_in_compound(&compound, "underlined").ok();
598 let strikethrough = get_in_compound(&compound, "strikethrough").ok();
599 let obfuscated = get_in_compound(&compound, "obfuscated").ok();
600 let click_event = get_in_compound(&compound, "click_event").ok();
601 let insertion = get_in_compound(&compound, "insertion").ok();
604 let font = get_in_compound(&compound, "font").ok();
605 Ok(Style {
606 color,
607 shadow_color,
608 bold,
609 italic,
610 underlined,
611 strikethrough,
612 obfuscated,
613 click_event,
614 hover_event: None,
615 insertion,
616 font,
617 })
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn text_color_named_colors() {
627 assert_eq!(TextColor::parse("red").unwrap().value, 16733525);
628 }
629 #[test]
630 fn text_color_hex_colors() {
631 assert_eq!(TextColor::parse("#a1b2c3").unwrap().value, 10597059);
632 }
633
634 #[test]
635 fn ansi_difference_should_reset() {
636 let style_a = Style {
637 bold: Some(true),
638 italic: Some(true),
639 ..Style::default()
640 };
641 let style_b = Style {
642 bold: Some(false),
643 italic: Some(true),
644 ..Style::default()
645 };
646 let ansi_difference = style_a.compare_ansi(&style_b);
647 assert_eq!(
648 ansi_difference,
649 format!(
650 "{reset}{italic}",
651 reset = Ansi::RESET,
652 italic = Ansi::ITALIC
653 )
654 )
655 }
656 #[test]
657 fn ansi_difference_shouldnt_reset() {
658 let style_a = Style {
659 bold: Some(true),
660 ..Style::default()
661 };
662 let style_b = Style {
663 bold: Some(true),
664 italic: Some(true),
665 ..Style::default()
666 };
667 let ansi_difference = style_a.compare_ansi(&style_b);
668 assert_eq!(ansi_difference, Ansi::ITALIC)
669 }
670
671 #[test]
672 fn test_from_code() {
673 assert_eq!(
674 ChatFormatting::from_code('a').unwrap(),
675 ChatFormatting::Green
676 );
677 }
678
679 #[test]
680 fn test_apply_formatting() {
681 let mut style = Style::default();
682 style.apply_formatting(&ChatFormatting::Bold);
683 style.apply_formatting(&ChatFormatting::Red);
684 assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
685 }
686}