kyori_component_json/
lib.rs

1//! # kyori-component-json
2//!
3//! A library for serialising and deserialising Minecraft's JSON component format, also known as
4//! 'raw JSON text'. Minecraft uses this format to display rich text throughout the game, including in
5//! commands such as /tellraw and in elements such as books, signs, scoreboards and entity names.
6//!
7//! ## Features
8//! - Full support for Minecraft's component specification (as of Java Edition 1.21.5+)
9//! - Serialization and deserialization using Serde
10//! - Builder-style API for constructing components
11//! - Style inheritance and component nesting
12//! - Comprehensive type safety for all component elements
13//!
14//! ## When to Use
15//! This library is useful when:
16//! - Generating complex chat messages with formatting and interactivity
17//! - Creating custom books or signs with rich text
18//! - Building command generators that use `/tellraw` or `/title`
19//! - Processing component data from Minecraft APIs or data packs
20//!
21//! ## Getting Started
22//! Add to your `Cargo.toml`:
23//! ```toml
24//! [dependencies]
25//! kyori-component-json = "0.1"
26//! serde-json = "1.0"
27//! ```
28//!
29//! ## Basic Example
30//! ```
31//! use kyori_component_json::*;
32//! use serde_json::json;
33//!
34//! // Create a formatted chat message
35//! let message = Component::text("Hello ")
36//!     .color(Some(Color::Named(NamedColor::Yellow)))
37//!     .append(
38//!         Component::text("World!")
39//!             .color(Some(Color::Named(NamedColor::White)))
40//!             .decoration(TextDecoration::Bold, Some(true))
41//!     )
42//!     .append_newline()
43//!     .append(
44//!         Component::text("Click here")
45//!             .click_event(Some(ClickEvent::RunCommand {
46//!                 command: "/say I was clicked!".into()
47//!             }))
48//!             .hover_event(Some(HoverEvent::ShowText {
49//!                 value: Component::text("Run a command!")
50//!             }))
51//!     );
52//!
53//! // Serialize to JSON
54//! let json = serde_json::to_value(&message).unwrap();
55//! assert_eq!(json, json!({
56//!     "text": "Hello ",
57//!     "color": "yellow",
58//!     "extra": [
59//!         {
60//!             "text": "World!",
61//!             "color": "white",
62//!             "bold": true
63//!         },
64//!         {"text": "\n"},
65//!         {
66//!             "text": "Click here",
67//!             "click_event": {
68//!                 "action": "run_command",
69//!                 "command": "/say I was clicked!"
70//!             },
71//!             "hover_event": {
72//!                 "action": "show_text",
73//!                 "value": {"text": "Run a command!"}
74//!             }
75//!         }
76//!     ]
77//! }));
78//! ```
79//!
80//! ## Key Concepts
81//! 1. **Components** - The building blocks of Minecraft text:
82//!    - `String`: Plain text shorthand
83//!    - `Array`: List of components
84//!    - `Object`: Full component with properties
85//! 2. **Content Types** - Special content like translations or scores
86//! 3. **Formatting** - Colors, styles, and fonts
87//! 4. **Interactivity** - Click and hover events
88//!
89//! See [Minecraft Wiki](https://minecraft.wiki/w/Text_component_format) for full specification.
90#![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/// Represents a Minecraft text component. Allows de/serialization using Serde with JSON.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(untagged)]
108pub enum Component {
109    /// Simple string component (shorthand for `{text: "value"}`)
110    String(String),
111    /// Array of components (shorthand for first component with extras)
112    Array(Vec<Component>),
113    /// Full component object with properties
114    Object(Box<ComponentObject>),
115}
116
117/// Content type of a component object
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum ContentType {
121    /// Plain text content
122    Text,
123    /// Localized translation text
124    Translatable,
125    /// Scoreboard value
126    Score,
127    /// Entity selector
128    Selector,
129    /// Key binding display
130    Keybind,
131    /// NBT data display
132    Nbt,
133}
134
135/// Named text colors from Minecraft
136#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum NamedColor {
139    /// #000000
140    Black,
141    /// #0000AA
142    DarkBlue,
143    /// #00AA00
144    DarkGreen,
145    /// #00AAAA
146    DarkAqua,
147    /// #AA0000
148    DarkRed,
149    /// #AA00AA
150    DarkPurple,
151    /// #FFAA00
152    Gold,
153    /// #AAAAAA
154    Gray,
155    /// #555555
156    DarkGray,
157    /// #5555FF
158    Blue,
159    /// #55FF55
160    Green,
161    /// #55FFFF
162    Aqua,
163    /// #FF5555
164    Red,
165    /// #FF55FF
166    LightPurple,
167    /// #FFFF55
168    Yellow,
169    /// #FFFFFF
170    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            // look for case-insensitive match by name
179            .find(|(name, _)| name.eq_ignore_ascii_case(s))
180            // and return the color
181            .map(|(_, color)| *color)
182            // if not found, return error
183            .ok_or(())
184    }
185}
186
187/// Text color representation (either named or hex)
188#[derive(Debug, Clone, PartialEq, Eq, Hash)]
189pub enum Color {
190    /// Predefined Minecraft color name
191    Named(NamedColor),
192    /// Hex color code in #RRGGBB format
193    Hex(String),
194}
195
196impl Color {
197    /// Gets the named color for a given `Color`.
198    /// If the color is not a named color, it will try to find a matching named color for a hex color.
199    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/// Shadow color representation (integer or float array)
258#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
259#[serde(untagged)]
260pub enum ShadowColor {
261    /// RGBA packed as 32-bit integer (0xRRGGBBAA)
262    Int(i32),
263    /// RGBA as [0.0-1.0] float values
264    Floats([f32; 4]),
265}
266
267/// Actions triggered when clicking text
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(rename_all = "snake_case", tag = "action")]
270#[allow(missing_docs)]
271pub enum ClickEvent {
272    /// Open URL in browser
273    OpenUrl { url: String },
274    /// Open file (client-side only)
275    OpenFile { path: String },
276    /// Execute command
277    RunCommand { command: String },
278    /// Suggest command in chat
279    SuggestCommand { command: String },
280    /// Change page in books
281    ChangePage { page: i32 },
282    /// Copy text to clipboard
283    CopyToClipboard { value: String },
284}
285
286/// UUID representation for entity hover events
287#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
288#[serde(untagged)]
289pub enum UuidRepr {
290    /// String representation (hyphenated hex format)
291    String(String),
292    /// Integer array representation
293    IntArray([i32; 4]),
294}
295
296/// Information shown when hovering over text
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298#[serde(rename_all = "snake_case", tag = "action")]
299pub enum HoverEvent {
300    /// Show text component
301    ShowText {
302        /// Text to display
303        value: Component,
304    },
305    /// Show item tooltip
306    ShowItem {
307        /// Item ID (e.g., "minecraft:diamond_sword")
308        id: String,
309        /// Stack count
310        #[serde(skip_serializing_if = "Option::is_none")]
311        count: Option<i32>,
312        /// Additional item components
313        #[serde(skip_serializing_if = "Option::is_none")]
314        components: Option<Value>,
315    },
316    /// Show entity information
317    ShowEntity {
318        /// Custom name override
319        #[serde(skip_serializing_if = "Option::is_none")]
320        name: Option<Component>,
321        /// Entity type ID
322        id: String,
323        /// Entity UUID
324        uuid: UuidRepr,
325    },
326}
327
328/// Scoreboard value content
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330pub struct ScoreContent {
331    /// Score holder (player name or selector)
332    pub name: String,
333    /// Objective name
334    pub objective: String,
335}
336
337/// Source for NBT data
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum NbtSource {
341    /// Block entity data
342    Block,
343    /// Entity data
344    Entity,
345    /// Command storage
346    Storage,
347}
348
349/// Core component structure containing all properties
350#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
351#[serde(deny_unknown_fields)]
352pub struct ComponentObject {
353    /// Content type specification
354    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
355    pub content_type: Option<ContentType>,
356
357    /// Plain text content
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub text: Option<String>,
360
361    /// Translation key
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub translate: Option<String>,
364
365    /// Fallback text for missing translations
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub fallback: Option<String>,
368
369    /// Arguments for translations
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub with: Option<Vec<Component>>,
372
373    /// Scoreboard value
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub score: Option<ScoreContent>,
376
377    /// Entity selector
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub selector: Option<String>,
380
381    /// Custom separator for multi-value components
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub separator: Option<Box<Component>>,
384
385    /// Key binding name
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub keybind: Option<String>,
388
389    /// NBT path query
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub nbt: Option<String>,
392
393    /// NBT source type
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub source: Option<NbtSource>,
396
397    /// Whether to interpret NBT as components
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub interpret: Option<bool>,
400
401    /// Block coordinates for NBT source
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub block: Option<String>,
404
405    /// Entity selector for NBT source
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub entity: Option<String>,
408
409    /// Storage ID for NBT source
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub storage: Option<String>,
412
413    /// Child components
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub extra: Option<Vec<Component>>,
416
417    /// Text color
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub color: Option<Color>,
420
421    /// Font resource location
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub font: Option<String>,
424
425    /// Bold formatting
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub bold: Option<bool>,
428
429    /// Italic formatting
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub italic: Option<bool>,
432
433    /// Underline formatting
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub underlined: Option<bool>,
436
437    /// Strikethrough formatting
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub strikethrough: Option<bool>,
440
441    /// Obfuscated text
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub obfuscated: Option<bool>,
444
445    /// Text shadow color
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub shadow_color: Option<ShadowColor>,
448
449    /// Text insertion on shift-click
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub insertion: Option<String>,
452
453    /// Click action
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub click_event: Option<ClickEvent>,
456
457    /// Hover action
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub hover_event: Option<HoverEvent>,
460}
461
462/// Style properties for components
463#[derive(Debug, Clone, Default, PartialEq)]
464pub struct Style {
465    /// Text color
466    pub color: Option<Color>,
467    /// Font resource location
468    pub font: Option<String>,
469    /// Bold formatting
470    pub bold: Option<bool>,
471    /// Italic formatting
472    pub italic: Option<bool>,
473    /// Underline formatting
474    pub underlined: Option<bool>,
475    /// Strikethrough formatting
476    pub strikethrough: Option<bool>,
477    /// Obfuscated text
478    pub obfuscated: Option<bool>,
479    /// Text shadow color
480    pub shadow_color: Option<ShadowColor>,
481    /// Text insertion on shift-click
482    pub insertion: Option<String>,
483    /// Click action
484    pub click_event: Option<ClickEvent>,
485    /// Hover action
486    pub hover_event: Option<HoverEvent>,
487}
488
489/// Text decoration styles
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
491pub enum TextDecoration {
492    /// Bold text
493    Bold,
494    /// Italic text
495    Italic,
496    /// Underlined text
497    Underlined,
498    /// Strikethrough text
499    Strikethrough,
500    /// Obfuscated (scrambled) text
501    Obfuscated,
502}
503
504/// Style properties for merging (unused in current implementation)
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
506pub enum StyleMerge {
507    /// Color property
508    Color,
509    /// Font property
510    Font,
511    /// Bold property
512    Bold,
513    /// Italic property
514    Italic,
515    /// Underline property
516    Underlined,
517    /// Strikethrough property
518    Strikethrough,
519    /// Obfuscated property
520    Obfuscated,
521    /// Shadow color property
522    ShadowColor,
523    /// Insertion property
524    Insertion,
525    /// Click event property
526    ClickEvent,
527    /// Hover event property
528    HoverEvent,
529}
530
531impl Component {
532    /// Creates a plain text component
533    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    /// Appends a child component
541    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    /// Appends a newline character
566    pub fn append_newline(self) -> Self {
567        self.append(Component::text("\n"))
568    }
569
570    /// Appends a space character
571    pub fn append_space(self) -> Self {
572        self.append(Component::text(" "))
573    }
574
575    /// Returns the "plain text" representation of this component as a [`Cow<str>`].
576    ///
577    /// This is the closest equivalent to [Kyori's plain text serializer](https://javadoc.io/doc/net.kyori/adventure-text-serializer-plain/latest/net/kyori/adventure/text/serializer/plain/PlainTextComponentSerializer.html)
578    ///
579    /// # Behavior by variant
580    ///
581    /// - `Component::String`: Returns a borrowed `&str` of the string content.
582    /// - `Component::Object`:  
583    ///     - If the object has `text` and no `extra` children, returns a borrowed `&str`.
584    ///     - If the object has `extra` children, returns an owned `String` concatenating
585    ///       the object's `text` (if any) with the `to_plain_text` of each child.
586    /// - `Component::Array`: Returns an owned `String` concatenating the `to_plain_text`
587    ///   of each element in the array.
588    ///
589    /// # Notes
590    ///
591    /// This method may allocate a new `String` if concatenation is needed.  
592    /// Use [`Self::get_plain_text`] if you only need a cheap, O(1) borrowed string from a single component.
593    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    /// Returns the raw `text` field if this component is a string or an object with a text field.
625    ///
626    /// Does not traverse children or consider other fields. Cheap, O(1) operation alternative to [`Self::to_plain_text`].
627    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    /// Applies fallback styles to unset properties
636    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    /// Sets text color
668    pub fn color(self, color: Option<Color>) -> Self {
669        self.map_object(|mut obj| {
670            obj.color = color;
671            obj
672        })
673    }
674
675    /// Sets font
676    pub fn font(self, font: Option<String>) -> Self {
677        self.map_object(|mut obj| {
678            obj.font = font;
679            obj
680        })
681    }
682
683    /// Sets text decoration state
684    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    /// Sets multiple decorations at once
698    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    /// Sets click event
714    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    /// Sets hover event
722    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    /// Sets insertion text
730    pub fn insertion(self, insertion: Option<String>) -> Self {
731        self.map_object(|mut obj| {
732            obj.insertion = insertion;
733            obj
734        })
735    }
736
737    /// Checks if a decoration is enabled
738    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    /// Checks if any styling is present
752    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    /// Sets child components
772    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    /// Gets child components
780    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    /// Internal method to apply transformations to component objects
789    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    /// Merges style properties from a fallback style
817    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/// Error type for color parsing
855#[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}