1#![warn(missing_docs)]
91#![warn(clippy::perf)]
92#![warn(clippy::unwrap_used, clippy::expect_used)]
93#![forbid(missing_copy_implementations, missing_debug_implementations)]
94#![forbid(unsafe_code)]
95
96mod colors;
97pub mod minimessage;
98pub mod parsing;
99
100use serde::{Deserialize, Serialize};
101use serde_json::Value;
102use std::borrow::Cow;
103use std::{collections::HashMap, fmt, str::FromStr};
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(untagged)]
108pub enum Component {
109 String(String),
111 Array(Vec<Component>),
113 Object(Box<ComponentObject>),
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum ContentType {
121 Text,
123 Translatable,
125 Score,
127 Selector,
129 Keybind,
131 Nbt,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum NamedColor {
139 Black,
141 DarkBlue,
143 DarkGreen,
145 DarkAqua,
147 DarkRed,
149 DarkPurple,
151 Gold,
153 Gray,
155 DarkGray,
157 Blue,
159 Green,
161 Aqua,
163 Red,
165 LightPurple,
167 Yellow,
169 White,
171}
172
173impl FromStr for NamedColor {
174 type Err = ();
175 fn from_str(s: &str) -> Result<Self, Self::Err> {
176 colors::NAME_TO_NAMED_COLOR
177 .iter()
178 .find(|(name, _)| name.eq_ignore_ascii_case(s))
180 .map(|(_, color)| *color)
182 .ok_or(())
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Hash)]
189pub enum Color {
190 Named(NamedColor),
192 Hex(String),
194}
195
196impl Color {
197 pub fn to_named(&self) -> Option<NamedColor> {
200 match self {
201 Color::Named(named) => Some(*named),
202 Color::Hex(hex) => colors::HEX_CODE_TO_NAMED_COLOR
203 .iter()
204 .find(|(h, _)| h == &hex.as_str())
205 .map(|(_, n)| *n),
206 }
207 }
208}
209
210impl fmt::Display for Color {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 match self {
213 Color::Named(named) => named.fmt(f),
214 Color::Hex(hex) => hex.fmt(f),
215 }
216 }
217}
218
219impl From<(u8, u8, u8)> for Color {
220 fn from((r, g, b): (u8, u8, u8)) -> Self {
221 Color::Hex(format!("#{:02X}{:02X}{:02X}", r, g, b))
222 }
223}
224
225impl From<[u8; 3]> for Color {
226 fn from([r, g, b]: [u8; 3]) -> Self {
227 Color::Hex(format!("#{:02X}{:02X}{:02X}", r, g, b))
228 }
229}
230
231impl Serialize for Color {
232 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
233 where
234 S: serde::Serializer,
235 {
236 match self {
237 Color::Named(named) => named.serialize(serializer),
238 Color::Hex(hex) => hex.serialize(serializer),
239 }
240 }
241}
242
243impl<'de> Deserialize<'de> for Color {
244 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
245 where
246 D: serde::Deserializer<'de>,
247 {
248 let s = String::deserialize(deserializer)?;
249 if let Ok(named) = serde_json::from_str::<NamedColor>(&format!("\"{s}\"")) {
250 Ok(Color::Named(named))
251 } else {
252 Ok(Color::Hex(s))
253 }
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
259#[serde(untagged)]
260pub enum ShadowColor {
261 Int(i32),
263 Floats([f32; 4]),
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(rename_all = "snake_case", tag = "action")]
270#[allow(missing_docs)]
271pub enum ClickEvent {
272 OpenUrl { url: String },
274 OpenFile { path: String },
276 RunCommand { command: String },
278 SuggestCommand { command: String },
280 ChangePage { page: i32 },
282 CopyToClipboard { value: String },
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
288#[serde(untagged)]
289pub enum UuidRepr {
290 String(String),
292 IntArray([i32; 4]),
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298#[serde(rename_all = "snake_case", tag = "action")]
299pub enum HoverEvent {
300 ShowText {
302 value: Component,
304 },
305 ShowItem {
307 id: String,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 count: Option<i32>,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 components: Option<Value>,
315 },
316 ShowEntity {
318 #[serde(skip_serializing_if = "Option::is_none")]
320 name: Option<Component>,
321 id: String,
323 uuid: UuidRepr,
325 },
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330pub struct ScoreContent {
331 pub name: String,
333 pub objective: String,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum NbtSource {
341 Block,
343 Entity,
345 Storage,
347}
348
349#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
351#[serde(deny_unknown_fields)]
352pub struct ComponentObject {
353 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
355 pub content_type: Option<ContentType>,
356
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub text: Option<String>,
360
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub translate: Option<String>,
364
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub fallback: Option<String>,
368
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub with: Option<Vec<Component>>,
372
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub score: Option<ScoreContent>,
376
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub selector: Option<String>,
380
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub separator: Option<Box<Component>>,
384
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub keybind: Option<String>,
388
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub nbt: Option<String>,
392
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub source: Option<NbtSource>,
396
397 #[serde(skip_serializing_if = "Option::is_none")]
399 pub interpret: Option<bool>,
400
401 #[serde(skip_serializing_if = "Option::is_none")]
403 pub block: Option<String>,
404
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub entity: Option<String>,
408
409 #[serde(skip_serializing_if = "Option::is_none")]
411 pub storage: Option<String>,
412
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub extra: Option<Vec<Component>>,
416
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub color: Option<Color>,
420
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub font: Option<String>,
424
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub bold: Option<bool>,
428
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub italic: Option<bool>,
432
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub underlined: Option<bool>,
436
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub strikethrough: Option<bool>,
440
441 #[serde(skip_serializing_if = "Option::is_none")]
443 pub obfuscated: Option<bool>,
444
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub shadow_color: Option<ShadowColor>,
448
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub insertion: Option<String>,
452
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub click_event: Option<ClickEvent>,
456
457 #[serde(skip_serializing_if = "Option::is_none")]
459 pub hover_event: Option<HoverEvent>,
460}
461
462#[derive(Debug, Clone, Default, PartialEq)]
464pub struct Style {
465 pub color: Option<Color>,
467 pub font: Option<String>,
469 pub bold: Option<bool>,
471 pub italic: Option<bool>,
473 pub underlined: Option<bool>,
475 pub strikethrough: Option<bool>,
477 pub obfuscated: Option<bool>,
479 pub shadow_color: Option<ShadowColor>,
481 pub insertion: Option<String>,
483 pub click_event: Option<ClickEvent>,
485 pub hover_event: Option<HoverEvent>,
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
491pub enum TextDecoration {
492 Bold,
494 Italic,
496 Underlined,
498 Strikethrough,
500 Obfuscated,
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
506pub enum StyleMerge {
507 Color,
509 Font,
511 Bold,
513 Italic,
515 Underlined,
517 Strikethrough,
519 Obfuscated,
521 ShadowColor,
523 Insertion,
525 ClickEvent,
527 HoverEvent,
529}
530
531impl Component {
532 pub fn text(text: impl AsRef<str>) -> Self {
534 Component::Object(Box::new(ComponentObject {
535 text: Some(text.as_ref().to_string()),
536 ..Default::default()
537 }))
538 }
539
540 pub fn append<C: Into<Component>>(self, component: C) -> Self {
542 let component = component.into();
543 match self {
544 Component::String(s) => Component::Object(Box::new(ComponentObject {
545 content_type: Some(ContentType::Text),
546 text: Some(s),
547 extra: Some(vec![component]),
548 ..Default::default()
549 })),
550 Component::Array(mut vec) => {
551 vec.push(component);
552 Component::Array(vec)
553 }
554 Component::Object(mut obj) => {
555 if let Some(extras) = &mut obj.extra {
556 extras.push(component);
557 } else {
558 obj.extra = Some(vec![component]);
559 }
560 Component::Object(obj)
561 }
562 }
563 }
564
565 pub fn append_newline(self) -> Self {
567 self.append(Component::text("\n"))
568 }
569
570 pub fn append_space(self) -> Self {
572 self.append(Component::text(" "))
573 }
574
575 pub fn to_plain_text(&self) -> Cow<'_, str> {
594 match self {
595 Component::String(s) => Cow::Borrowed(s),
596 Component::Object(obj) => {
597 if obj.extra.is_none()
598 && let Some(text) = &obj.text
599 {
600 return Cow::Borrowed(text);
601 }
602
603 let mut result = String::new();
604 if let Some(text) = &obj.text {
605 result.push_str(text);
606 }
607 if let Some(children) = &obj.extra {
608 for child in children {
609 result.push_str(&child.to_plain_text());
610 }
611 }
612 Cow::Owned(result)
613 }
614 Component::Array(components) => {
615 let mut result = String::new();
616 for c in components {
617 result.push_str(&c.to_plain_text());
618 }
619 Cow::Owned(result)
620 }
621 }
622 }
623
624 pub fn get_plain_text(&self) -> Option<&str> {
628 match self {
629 Component::String(s) => Some(s),
630 Component::Object(obj) => obj.text.as_deref(),
631 _ => None,
632 }
633 }
634
635 pub fn apply_fallback_style(self, fallback: &Style) -> Self {
637 match self {
638 Component::String(s) => {
639 let mut obj = ComponentObject {
640 content_type: Some(ContentType::Text),
641 text: Some(s),
642 ..Default::default()
643 };
644 obj.merge_style(fallback);
645 Component::Object(Box::new(obj))
646 }
647 Component::Array(vec) => Component::Array(
648 vec.into_iter()
649 .map(|c| c.apply_fallback_style(fallback))
650 .collect(),
651 ),
652 Component::Object(mut obj) => {
653 obj.merge_style(fallback);
654 if let Some(extras) = obj.extra {
655 obj.extra = Some(
656 extras
657 .into_iter()
658 .map(|c| c.apply_fallback_style(fallback))
659 .collect(),
660 );
661 }
662 Component::Object(obj)
663 }
664 }
665 }
666
667 pub fn color(self, color: Option<Color>) -> Self {
669 self.map_object(|mut obj| {
670 obj.color = color;
671 obj
672 })
673 }
674
675 pub fn font(self, font: Option<String>) -> Self {
677 self.map_object(|mut obj| {
678 obj.font = font;
679 obj
680 })
681 }
682
683 pub fn decoration(self, decoration: TextDecoration, state: Option<bool>) -> Self {
685 self.map_object(|mut obj| {
686 match decoration {
687 TextDecoration::Bold => obj.bold = state,
688 TextDecoration::Italic => obj.italic = state,
689 TextDecoration::Underlined => obj.underlined = state,
690 TextDecoration::Strikethrough => obj.strikethrough = state,
691 TextDecoration::Obfuscated => obj.obfuscated = state,
692 }
693 obj
694 })
695 }
696
697 pub fn decorations(self, decorations: &HashMap<TextDecoration, Option<bool>>) -> Self {
699 self.map_object(|mut obj| {
700 for (decoration, state) in decorations {
701 match decoration {
702 TextDecoration::Bold => obj.bold = *state,
703 TextDecoration::Italic => obj.italic = *state,
704 TextDecoration::Underlined => obj.underlined = *state,
705 TextDecoration::Strikethrough => obj.strikethrough = *state,
706 TextDecoration::Obfuscated => obj.obfuscated = *state,
707 }
708 }
709 obj
710 })
711 }
712
713 pub fn click_event(self, event: Option<ClickEvent>) -> Self {
715 self.map_object(|mut obj| {
716 obj.click_event = event;
717 obj
718 })
719 }
720
721 pub fn hover_event(self, event: Option<HoverEvent>) -> Self {
723 self.map_object(|mut obj| {
724 obj.hover_event = event;
725 obj
726 })
727 }
728
729 pub fn insertion(self, insertion: Option<String>) -> Self {
731 self.map_object(|mut obj| {
732 obj.insertion = insertion;
733 obj
734 })
735 }
736
737 pub fn has_decoration(&self, decoration: TextDecoration) -> bool {
739 match self {
740 Component::Object(obj) => match decoration {
741 TextDecoration::Bold => obj.bold.unwrap_or(false),
742 TextDecoration::Italic => obj.italic.unwrap_or(false),
743 TextDecoration::Underlined => obj.underlined.unwrap_or(false),
744 TextDecoration::Strikethrough => obj.strikethrough.unwrap_or(false),
745 TextDecoration::Obfuscated => obj.obfuscated.unwrap_or(false),
746 },
747 _ => false,
748 }
749 }
750
751 pub fn has_styling(&self) -> bool {
753 match self {
754 Component::Object(obj) => {
755 obj.color.is_some()
756 || obj.font.is_some()
757 || obj.bold.is_some()
758 || obj.italic.is_some()
759 || obj.underlined.is_some()
760 || obj.strikethrough.is_some()
761 || obj.obfuscated.is_some()
762 || obj.shadow_color.is_some()
763 || obj.insertion.is_some()
764 || obj.click_event.is_some()
765 || obj.hover_event.is_some()
766 }
767 _ => false,
768 }
769 }
770
771 pub fn set_children(self, children: Vec<Component>) -> Self {
773 self.map_object(|mut obj| {
774 obj.extra = Some(children);
775 obj
776 })
777 }
778
779 pub fn get_children(&self) -> &[Component] {
781 match self {
782 Component::Object(obj) => obj.extra.as_deref().unwrap_or_default(),
783 Component::Array(vec) => vec.as_slice(),
784 Component::String(_) => &[],
785 }
786 }
787
788 fn map_object<F>(self, f: F) -> Self
790 where
791 F: FnOnce(ComponentObject) -> ComponentObject,
792 {
793 match self {
794 Component::String(s) => {
795 let obj = ComponentObject {
796 content_type: Some(ContentType::Text),
797 text: Some(s),
798 ..Default::default()
799 };
800 Component::Object(Box::new(f(obj)))
801 }
802 Component::Array(vec) => {
803 let mut obj = ComponentObject {
804 extra: Some(vec),
805 ..Default::default()
806 };
807 obj = f(obj);
808 Component::Object(Box::new(obj))
809 }
810 Component::Object(obj) => Component::Object(Box::new(f(*obj))),
811 }
812 }
813}
814
815impl ComponentObject {
816 fn merge_style(&mut self, fallback: &Style) {
818 if self.color.is_none() {
819 self.color = fallback.color.clone();
820 }
821 if self.font.is_none() {
822 self.font = fallback.font.clone();
823 }
824 if self.bold.is_none() {
825 self.bold = fallback.bold;
826 }
827 if self.italic.is_none() {
828 self.italic = fallback.italic;
829 }
830 if self.underlined.is_none() {
831 self.underlined = fallback.underlined;
832 }
833 if self.strikethrough.is_none() {
834 self.strikethrough = fallback.strikethrough;
835 }
836 if self.obfuscated.is_none() {
837 self.obfuscated = fallback.obfuscated;
838 }
839 if self.shadow_color.is_none() {
840 self.shadow_color = fallback.shadow_color;
841 }
842 if self.insertion.is_none() {
843 self.insertion = fallback.insertion.clone();
844 }
845 if self.click_event.is_none() {
846 self.click_event = fallback.click_event.clone();
847 }
848 if self.hover_event.is_none() {
849 self.hover_event = fallback.hover_event.clone();
850 }
851 }
852}
853
854#[derive(Debug, Clone, Copy, PartialEq, Eq)]
856pub struct ParseColorError;
857
858impl std::fmt::Display for ParseColorError {
859 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860 write!(f, "invalid color format")
861 }
862}
863
864impl std::error::Error for ParseColorError {}
865
866fn parse_hex_color(s: &str) -> Option<[u8; 3]> {
867 let s = s.strip_prefix('#')?;
868 if s.len() == 6 {
869 let r = u8::from_str_radix(&s[0..2], 16).ok()?;
870 let g = u8::from_str_radix(&s[2..4], 16).ok()?;
871 let b = u8::from_str_radix(&s[4..6], 16).ok()?;
872 return Some([r, g, b]);
873 }
874 None
875}
876
877
878
879impl FromStr for Color {
880 type Err = ParseColorError;
881 fn from_str(s: &str) -> Result<Self, Self::Err> {
882 if let Ok(named) = s.parse::<NamedColor>() {
883 return Ok(Color::Named(named));
884 }
885
886 if parse_hex_color(s).is_some() {
887 return Ok(Color::Hex(s.to_string()));
888 }
889
890 Err(ParseColorError)
891 }
892}
893
894impl<T: AsRef<str>> From<T> for Component {
895 fn from(value: T) -> Component {
896 let s: &str = value.as_ref();
897 Component::String(s.to_string())
898 }
899}
900
901impl fmt::Display for NamedColor {
902 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
903 let s = match self {
904 NamedColor::Black => "black",
905 NamedColor::DarkBlue => "dark_blue",
906 NamedColor::DarkGreen => "dark_green",
907 NamedColor::DarkAqua => "dark_aqua",
908 NamedColor::DarkRed => "dark_red",
909 NamedColor::DarkPurple => "dark_purple",
910 NamedColor::Gold => "gold",
911 NamedColor::Gray => "gray",
912 NamedColor::DarkGray => "dark_gray",
913 NamedColor::Blue => "blue",
914 NamedColor::Green => "green",
915 NamedColor::Aqua => "aqua",
916 NamedColor::Red => "red",
917 NamedColor::LightPurple => "light_purple",
918 NamedColor::Yellow => "yellow",
919 NamedColor::White => "white",
920 };
921 write!(f, "{s}")
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928
929 #[test]
930 fn test_parse_message() {
931 let raw_json = r#"
932 {
933 "text": "Hello, ",
934 "color": "yellow",
935 "extra": [
936 {
937 "text": "World!",
938 "color": "white",
939 "bold": true
940 },
941 {
942 "translate": "chat.type.say",
943 "with": [
944 { "selector": "@p" }
945 ]
946 }
947 ]
948 }
949 "#;
950
951 let component: Component = serde_json::from_str(raw_json).unwrap();
952 println!("Message: {component:#?}");
953 }
954}