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, 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
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_string()),
416 font: j
417 .get("font")
418 .and_then(|v| v.as_str())
419 .map(|s| s.to_string()),
420 }
421 }
422
423 pub fn is_empty(&self) -> bool {
425 self.color.is_none()
426 && self.bold.is_none()
427 && self.italic.is_none()
428 && self.underlined.is_none()
429 && self.strikethrough.is_none()
430 && self.obfuscated.is_none()
431 }
432
433 pub fn compare_ansi(&self, after: &Style) -> String {
435 let should_reset =
436 (self.bold.unwrap_or_default() && !after.bold.unwrap_or_default()) ||
438 (self.italic.unwrap_or_default() && !after.italic.unwrap_or_default()) ||
440 (self.underlined.unwrap_or_default() && !after.underlined.unwrap_or_default()) ||
442 (self.strikethrough.unwrap_or_default() && !after.strikethrough.unwrap_or_default()) ||
444 (self.obfuscated.unwrap_or_default() && !after.obfuscated.unwrap_or_default());
446
447 let mut ansi_codes = String::new();
448
449 let empty_style = Style::empty();
450
451 let before = if should_reset {
452 ansi_codes.push_str(Ansi::RESET);
453 &empty_style
454 } else {
455 self
456 };
457
458 if !before.bold.unwrap_or_default() && after.bold.unwrap_or_default() {
460 ansi_codes.push_str(Ansi::BOLD);
461 }
462 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() {
468 ansi_codes.push_str(Ansi::UNDERLINED);
469 }
470 if !before.strikethrough.unwrap_or_default() && after.strikethrough.unwrap_or_default() {
473 ansi_codes.push_str(Ansi::STRIKETHROUGH);
474 }
475 if !before.obfuscated.unwrap_or_default() && after.obfuscated.unwrap_or_default() {
477 ansi_codes.push_str(Ansi::OBFUSCATED);
478 }
479
480 let color_changed = {
482 if before.color.is_none() && after.color.is_some() {
483 true
484 } else if before.color.is_some() && after.color.is_some() {
485 before.color.clone().unwrap().value != after.color.as_ref().unwrap().value
486 } else {
487 false
488 }
489 };
490
491 if color_changed {
492 let after_color = after.color.as_ref().unwrap();
493 ansi_codes.push_str(&Ansi::rgb(after_color.value));
494 }
495
496 ansi_codes
497 }
498
499 pub fn merged_with(&self, other: &Style) -> Style {
503 Style {
504 color: other.color.clone().or(self.color.clone()),
505 shadow_color: other.shadow_color.or(self.shadow_color),
506 bold: other.bold.or(self.bold),
507 italic: other.italic.or(self.italic),
508 underlined: other.underlined.or(self.underlined),
509 strikethrough: other.strikethrough.or(self.strikethrough),
510 obfuscated: other.obfuscated.or(self.obfuscated),
511 click_event: other.click_event.clone().or(self.click_event.clone()),
512 hover_event: other.hover_event.clone().or(self.hover_event.clone()),
513 insertion: other.insertion.clone().or(self.insertion.clone()),
514 font: other.font.clone().or(self.font.clone()),
515 }
516 }
517
518 pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
520 match *formatting {
521 ChatFormatting::Bold => self.bold = Some(true),
522 ChatFormatting::Italic => self.italic = Some(true),
523 ChatFormatting::Underline => self.underlined = Some(true),
524 ChatFormatting::Strikethrough => self.strikethrough = Some(true),
525 ChatFormatting::Obfuscated => self.obfuscated = Some(true),
526 ChatFormatting::Reset => {
527 self.color = None;
528 self.bold = None;
529 self.italic = None;
530 self.underlined = None;
531 self.strikethrough = None;
532 self.obfuscated = None;
533 }
534 formatter => {
535 if let Some(color) = formatter.color() {
537 self.color = Some(TextColor::from_rgb(color));
538 }
539 }
540 }
541 }
542
543 pub fn get_html_style(&self) -> String {
544 let mut style = String::new();
545 if let Some(color) = &self.color {
546 style.push_str(&format!("color:{};", color.format_value()));
547 }
548 if let Some(bold) = self.bold {
549 style.push_str(&format!(
550 "font-weight:{};",
551 if bold { "bold" } else { "normal" }
552 ));
553 }
554 if let Some(italic) = self.italic {
555 style.push_str(&format!(
556 "font-style:{};",
557 if italic { "italic" } else { "normal" }
558 ));
559 }
560 if let Some(underlined) = self.underlined {
561 style.push_str(&format!(
562 "text-decoration:{};",
563 if underlined { "underline" } else { "none" }
564 ));
565 }
566 if let Some(strikethrough) = self.strikethrough {
567 style.push_str(&format!(
568 "text-decoration:{};",
569 if strikethrough {
570 "line-through"
571 } else {
572 "none"
573 }
574 ));
575 }
576 if let Some(obfuscated) = self.obfuscated
577 && obfuscated
578 {
579 style.push_str("filter:blur(2px);");
580 }
581
582 style
583 }
584}
585
586#[cfg(feature = "simdnbt")]
587impl simdnbt::Deserialize for Style {
588 fn from_compound(
589 compound: simdnbt::borrow::NbtCompound,
590 ) -> Result<Self, simdnbt::DeserializeError> {
591 let bold = compound.byte("bold").map(|v| v != 0);
592 let italic = compound.byte("italic").map(|v| v != 0);
593 let underlined = compound.byte("underlined").map(|v| v != 0);
594 let strikethrough = compound.byte("strikethrough").map(|v| v != 0);
595 let obfuscated = compound.byte("obfuscated").map(|v| v != 0);
596 let color: Option<TextColor> = compound
597 .string("color")
598 .and_then(|v| TextColor::parse(&v.to_str()));
599 Ok(Style {
600 color,
601 bold,
602 italic,
603 underlined,
604 strikethrough,
605 obfuscated,
606 ..Style::default()
607 })
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614
615 #[test]
616 fn text_color_named_colors() {
617 assert_eq!(TextColor::parse("red").unwrap().value, 16733525);
618 }
619 #[test]
620 fn text_color_hex_colors() {
621 assert_eq!(TextColor::parse("#a1b2c3").unwrap().value, 10597059);
622 }
623
624 #[test]
625 fn ansi_difference_should_reset() {
626 let style_a = Style {
627 bold: Some(true),
628 italic: Some(true),
629 ..Style::default()
630 };
631 let style_b = Style {
632 bold: Some(false),
633 italic: Some(true),
634 ..Style::default()
635 };
636 let ansi_difference = style_a.compare_ansi(&style_b);
637 assert_eq!(
638 ansi_difference,
639 format!(
640 "{reset}{italic}",
641 reset = Ansi::RESET,
642 italic = Ansi::ITALIC
643 )
644 )
645 }
646 #[test]
647 fn ansi_difference_shouldnt_reset() {
648 let style_a = Style {
649 bold: Some(true),
650 ..Style::default()
651 };
652 let style_b = Style {
653 bold: Some(true),
654 italic: Some(true),
655 ..Style::default()
656 };
657 let ansi_difference = style_a.compare_ansi(&style_b);
658 assert_eq!(ansi_difference, Ansi::ITALIC)
659 }
660
661 #[test]
662 fn test_from_code() {
663 assert_eq!(
664 ChatFormatting::from_code('a').unwrap(),
665 ChatFormatting::Green
666 );
667 }
668
669 #[test]
670 fn test_apply_formatting() {
671 let mut style = Style::default();
672 style.apply_formatting(&ChatFormatting::Bold);
673 style.apply_formatting(&ChatFormatting::Red);
674 assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
675 }
676}