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 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_owned()),
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, Debug, Eq, Hash, PartialEq)]
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_owned())))
304 }
305}
306
307macro_rules! define_style_struct {
308 ($($(#[$doc:meta])* $field:ident : $type:ty),* $(,)?) => {
309 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
310 #[non_exhaustive]
311 pub struct Style {
312 $(
313 #[serde(skip_serializing_if = "Option::is_none")]
314 $(#[$doc])*
315 pub $field: Option<$type>,
316 )*
317 }
318
319 impl Style {
320 $(
321 pub fn $field(mut self, value: impl Into<Option<$type>>) -> Self {
322 self.$field = value.into();
323 self
324 }
325 )*
326
327 pub fn serialize_map<S>(&self, state: &mut S::SerializeMap) -> Result<(), S::Error>
328 where
329 S: serde::Serializer,
330 {
331 $(
332 if let Some(value) = &self.$field {
333 state.serialize_entry(stringify!($field), value)?;
334 }
335 )*
336 Ok(())
337 }
338
339 pub fn apply(&mut self, style: &Style) {
341 $(
342 if let Some(value) = &style.$field {
343 self.$field = Some(value.clone());
344 }
345 )*
346 }
347 }
348
349 #[cfg(feature = "simdnbt")]
350 impl simdnbt::Serialize for Style {
351 fn to_compound(self) -> NbtCompound {
352 let mut compound = NbtCompound::new();
353
354 $(
355 if let Some(value) = self.$field {
356 compound.insert(stringify!($field), value);
357 }
358 )*
359
360 compound
361 }
362 }
363 };
364}
365
366define_style_struct! {
367 color: TextColor,
368 shadow_color: u32,
369 bold: bool,
370 italic: bool,
371 underlined: bool,
372 strikethrough: bool,
373 obfuscated: bool,
374 click_event: ClickEvent,
375 hover_event: HoverEvent,
376 insertion: String,
377 font: String,
379}
380
381impl Style {
382 pub fn new() -> Self {
383 Self::default()
384 }
385
386 pub fn empty() -> Self {
387 Self::default()
388 }
389
390 pub fn deserialize(json: &Value) -> Style {
391 let Some(j) = json.as_object() else {
392 return Style::default();
393 };
394
395 Style {
396 color: j
397 .get("color")
398 .and_then(|v| v.as_str())
399 .and_then(TextColor::parse),
400 shadow_color: j
401 .get("shadow_color")
402 .and_then(|v| v.as_u64())
403 .map(|v| v as u32),
404 bold: j.get("bold").and_then(|v| v.as_bool()),
405 italic: j.get("italic").and_then(|v| v.as_bool()),
406 underlined: j.get("underlined").and_then(|v| v.as_bool()),
407 strikethrough: j.get("strikethrough").and_then(|v| v.as_bool()),
408 obfuscated: j.get("obfuscated").and_then(|v| v.as_bool()),
409 click_event: Default::default(),
411 hover_event: Default::default(),
412 insertion: j
413 .get("insertion")
414 .and_then(|v| v.as_str())
415 .map(|s| s.to_owned()),
416 font: j.get("font").and_then(|v| v.as_str()).map(|s| s.to_owned()),
417 }
418 }
419
420 pub fn is_empty(&self) -> bool {
422 self.color.is_none()
423 && self.bold.is_none()
424 && self.italic.is_none()
425 && self.underlined.is_none()
426 && self.strikethrough.is_none()
427 && self.obfuscated.is_none()
428 }
429
430 pub fn compare_ansi(&self, after: &Style) -> String {
432 let should_reset =
433 (self.bold.unwrap_or_default() && !after.bold.unwrap_or_default()) ||
435 (self.italic.unwrap_or_default() && !after.italic.unwrap_or_default()) ||
437 (self.underlined.unwrap_or_default() && !after.underlined.unwrap_or_default()) ||
439 (self.strikethrough.unwrap_or_default() && !after.strikethrough.unwrap_or_default()) ||
441 (self.obfuscated.unwrap_or_default() && !after.obfuscated.unwrap_or_default());
443
444 let mut ansi_codes = String::new();
445
446 let empty_style = Style::empty();
447
448 let before = if should_reset {
449 ansi_codes.push_str(Ansi::RESET);
450 &empty_style
451 } else {
452 self
453 };
454
455 if !before.bold.unwrap_or_default() && after.bold.unwrap_or_default() {
457 ansi_codes.push_str(Ansi::BOLD);
458 }
459 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() {
465 ansi_codes.push_str(Ansi::UNDERLINED);
466 }
467 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() {
474 ansi_codes.push_str(Ansi::OBFUSCATED);
475 }
476
477 let color_changed = {
479 if before.color.is_none() && after.color.is_some() {
480 true
481 } else if before.color.is_some() && after.color.is_some() {
482 before.color.clone().unwrap().value != after.color.as_ref().unwrap().value
483 } else {
484 false
485 }
486 };
487
488 if color_changed {
489 let after_color = after.color.as_ref().unwrap();
490 ansi_codes.push_str(&Ansi::rgb(after_color.value));
491 }
492
493 ansi_codes
494 }
495
496 pub fn merged_with(&self, other: &Style) -> Style {
500 Style {
501 color: other.color.clone().or(self.color.clone()),
502 shadow_color: other.shadow_color.or(self.shadow_color),
503 bold: other.bold.or(self.bold),
504 italic: other.italic.or(self.italic),
505 underlined: other.underlined.or(self.underlined),
506 strikethrough: other.strikethrough.or(self.strikethrough),
507 obfuscated: other.obfuscated.or(self.obfuscated),
508 click_event: other.click_event.clone().or(self.click_event.clone()),
509 hover_event: other.hover_event.clone().or(self.hover_event.clone()),
510 insertion: other.insertion.clone().or(self.insertion.clone()),
511 font: other.font.clone().or(self.font.clone()),
512 }
513 }
514
515 pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
517 match *formatting {
518 ChatFormatting::Bold => self.bold = Some(true),
519 ChatFormatting::Italic => self.italic = Some(true),
520 ChatFormatting::Underline => self.underlined = Some(true),
521 ChatFormatting::Strikethrough => self.strikethrough = Some(true),
522 ChatFormatting::Obfuscated => self.obfuscated = Some(true),
523 ChatFormatting::Reset => {
524 self.color = None;
525 self.bold = None;
526 self.italic = None;
527 self.underlined = None;
528 self.strikethrough = None;
529 self.obfuscated = None;
530 }
531 formatter => {
532 if let Some(color) = formatter.color() {
534 self.color = Some(TextColor::from_rgb(color));
535 }
536 }
537 }
538 }
539
540 pub fn get_html_style(&self) -> String {
541 let mut style = String::new();
542 if let Some(color) = &self.color {
543 style.push_str(&format!("color:{};", color.format_value()));
544 }
545 if let Some(bold) = self.bold {
546 style.push_str(&format!(
547 "font-weight:{};",
548 if bold { "bold" } else { "normal" }
549 ));
550 }
551 if let Some(italic) = self.italic {
552 style.push_str(&format!(
553 "font-style:{};",
554 if italic { "italic" } else { "normal" }
555 ));
556 }
557 if let Some(underlined) = self.underlined {
558 style.push_str(&format!(
559 "text-decoration:{};",
560 if underlined { "underline" } else { "none" }
561 ));
562 }
563 if let Some(strikethrough) = self.strikethrough {
564 style.push_str(&format!(
565 "text-decoration:{};",
566 if strikethrough {
567 "line-through"
568 } else {
569 "none"
570 }
571 ));
572 }
573 if let Some(obfuscated) = self.obfuscated
574 && obfuscated
575 {
576 style.push_str("filter:blur(2px);");
577 }
578
579 style
580 }
581}
582
583#[cfg(feature = "simdnbt")]
584impl simdnbt::Deserialize for Style {
585 fn from_compound(
586 compound: simdnbt::borrow::NbtCompound,
587 ) -> Result<Self, simdnbt::DeserializeError> {
588 let bold = compound.byte("bold").map(|v| v != 0);
589 let italic = compound.byte("italic").map(|v| v != 0);
590 let underlined = compound.byte("underlined").map(|v| v != 0);
591 let strikethrough = compound.byte("strikethrough").map(|v| v != 0);
592 let obfuscated = compound.byte("obfuscated").map(|v| v != 0);
593 let color: Option<TextColor> = compound
594 .string("color")
595 .and_then(|v| TextColor::parse(&v.to_str()));
596 Ok(Style {
597 color,
598 bold,
599 italic,
600 underlined,
601 strikethrough,
602 obfuscated,
603 ..Style::default()
604 })
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 #[test]
613 fn text_color_named_colors() {
614 assert_eq!(TextColor::parse("red").unwrap().value, 16733525);
615 }
616 #[test]
617 fn text_color_hex_colors() {
618 assert_eq!(TextColor::parse("#a1b2c3").unwrap().value, 10597059);
619 }
620
621 #[test]
622 fn ansi_difference_should_reset() {
623 let style_a = Style {
624 bold: Some(true),
625 italic: Some(true),
626 ..Style::default()
627 };
628 let style_b = Style {
629 bold: Some(false),
630 italic: Some(true),
631 ..Style::default()
632 };
633 let ansi_difference = style_a.compare_ansi(&style_b);
634 assert_eq!(
635 ansi_difference,
636 format!(
637 "{reset}{italic}",
638 reset = Ansi::RESET,
639 italic = Ansi::ITALIC
640 )
641 )
642 }
643 #[test]
644 fn ansi_difference_shouldnt_reset() {
645 let style_a = Style {
646 bold: Some(true),
647 ..Style::default()
648 };
649 let style_b = Style {
650 bold: Some(true),
651 italic: Some(true),
652 ..Style::default()
653 };
654 let ansi_difference = style_a.compare_ansi(&style_b);
655 assert_eq!(ansi_difference, Ansi::ITALIC)
656 }
657
658 #[test]
659 fn test_from_code() {
660 assert_eq!(
661 ChatFormatting::from_code('a').unwrap(),
662 ChatFormatting::Green
663 );
664 }
665
666 #[test]
667 fn test_apply_formatting() {
668 let mut style = Style::default();
669 style.apply_formatting(&ChatFormatting::Bold);
670 style.apply_formatting(&ChatFormatting::Red);
671 assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
672 }
673}