valence_text/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4use std::ops::{Deref, DerefMut};
5use std::str::FromStr;
6use std::{fmt, ops};
7
8use serde::de::Visitor;
9use serde::{de, Deserialize, Deserializer, Serialize};
10use uuid::Uuid;
11use valence_ident::Ident;
12use valence_nbt::Value;
13
14pub mod color;
15mod into_text;
16#[cfg(test)]
17mod tests;
18
19pub use color::Color;
20pub use into_text::IntoText;
21
22/// Represents formatted text in Minecraft's JSON text format.
23///
24/// Text is used in various places such as chat, window titles,
25/// disconnect messages, written books, signs, and more.
26///
27/// For more information, see the relevant [Minecraft Wiki article].
28///
29/// [Minecraft Wiki article]: https://minecraft.wiki/w/Raw_JSON_text_format
30///
31/// # Examples
32///
33/// With [`IntoText`] in scope, you can write the following:
34/// ```
35/// use valence_text::{Color, IntoText, Text};
36///
37/// let txt = "The text is ".into_text()
38///     + "Red".color(Color::RED)
39///     + ", "
40///     + "Green".color(Color::GREEN)
41///     + ", and also "
42///     + "Blue".color(Color::BLUE)
43///     + "! And maybe even "
44///     + "Italic".italic()
45///     + ".";
46///
47/// assert_eq!(
48///     txt.to_string(),
49///     r#"{"text":"The text is ","extra":[{"text":"Red","color":"red"},{"text":", "},{"text":"Green","color":"green"},{"text":", and also "},{"text":"Blue","color":"blue"},{"text":"! And maybe even "},{"text":"Italic","italic":true},{"text":"."}]}"#
50/// );
51/// ```
52#[derive(Clone, PartialEq, Default, Serialize)]
53#[serde(transparent)]
54pub struct Text(Box<TextInner>);
55
56/// Text data and formatting.
57#[derive(Clone, PartialEq, Default, Debug, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct TextInner {
60    #[serde(flatten)]
61    pub content: TextContent,
62
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub color: Option<Color>,
65
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub font: Option<Font>,
68
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub bold: Option<bool>,
71
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub italic: Option<bool>,
74
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub underlined: Option<bool>,
77
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub strikethrough: Option<bool>,
80
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub obfuscated: Option<bool>,
83
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub insertion: Option<Cow<'static, str>>,
86
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub click_event: Option<ClickEvent>,
89
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub hover_event: Option<HoverEvent>,
92
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub extra: Vec<Text>,
95}
96
97/// The text content of a Text object.
98#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
99#[serde(untagged)]
100pub enum TextContent {
101    /// Normal text
102    Text { text: Cow<'static, str> },
103    /// A piece of text that will be translated on the client based on the
104    /// client language. If no corresponding translation can be found, the
105    /// identifier itself is used as the translated text.
106    Translate {
107        /// A translation identifier, corresponding to the identifiers found in
108        /// loaded language files.
109        translate: Cow<'static, str>,
110        /// Optional list of text components to be inserted into slots in the
111        /// translation text. Ignored if `translate` is not present.
112        #[serde(default, skip_serializing_if = "Vec::is_empty")]
113        with: Vec<Text>,
114    },
115    /// Displays a score holder's current score in an objective.
116    ScoreboardValue { score: ScoreboardValueContent },
117    /// Displays the name of one or more entities found by a [`selector`].
118    ///
119    /// [`selector`]: https://minecraft.wiki/w/Target_selectors
120    EntityNames {
121        /// A string containing a [`selector`].
122        ///
123        /// [`selector`]: https://minecraft.wiki/w/Target_selectors
124        selector: Cow<'static, str>,
125        /// An optional custom separator used when the selector returns multiple
126        /// entities. Defaults to the ", " text with gray color.
127        #[serde(default, skip_serializing_if = "Option::is_none")]
128        separator: Option<Text>,
129    },
130    /// Displays the name of the button that is currently bound to a certain
131    /// configurable control on the client.
132    Keybind {
133        /// A [`keybind identifier`], to be displayed as the name of the button
134        /// that is currently bound to that action.
135        ///
136        /// [`keybind identifier`]: https://minecraft.wiki/w/Controls#Configurable_controls
137        keybind: Cow<'static, str>,
138    },
139    /// Displays NBT values from block entities.
140    BlockNbt {
141        block: Cow<'static, str>,
142        nbt: Cow<'static, str>,
143        #[serde(default, skip_serializing_if = "Option::is_none")]
144        interpret: Option<bool>,
145        #[serde(default, skip_serializing_if = "Option::is_none")]
146        separator: Option<Text>,
147    },
148    /// Displays NBT values from entities.
149    EntityNbt {
150        entity: Cow<'static, str>,
151        nbt: Cow<'static, str>,
152        #[serde(default, skip_serializing_if = "Option::is_none")]
153        interpret: Option<bool>,
154        #[serde(default, skip_serializing_if = "Option::is_none")]
155        separator: Option<Text>,
156    },
157    /// Displays NBT values from command storage.
158    StorageNbt {
159        storage: Ident<Cow<'static, str>>,
160        nbt: Cow<'static, str>,
161        #[serde(default, skip_serializing_if = "Option::is_none")]
162        interpret: Option<bool>,
163        #[serde(default, skip_serializing_if = "Option::is_none")]
164        separator: Option<Text>,
165    },
166}
167
168/// Scoreboard value.
169#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
170pub struct ScoreboardValueContent {
171    /// The name of the score holder whose score should be displayed. This
172    /// can be a [`selector`] or an explicit name.
173    ///
174    /// [`selector`]: https://minecraft.wiki/w/Target_selectors
175    pub name: Cow<'static, str>,
176    /// The internal name of the objective to display the player's score in.
177    pub objective: Cow<'static, str>,
178    /// If present, this value is displayed regardless of what the score
179    /// would have been.
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub value: Option<Cow<'static, str>>,
182}
183
184/// Action to take on click of the text.
185#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
186#[serde(tag = "action", content = "value", rename_all = "snake_case")]
187pub enum ClickEvent {
188    /// Opens an URL
189    OpenUrl(Cow<'static, str>),
190    /// Only usable by internal servers for security reasons.
191    OpenFile(Cow<'static, str>),
192    /// Sends a chat command. Doesn't actually have to be a command, can be a
193    /// normal chat message.
194    RunCommand(Cow<'static, str>),
195    /// Replaces the contents of the chat box with the text, not necessarily a
196    /// command.
197    SuggestCommand(Cow<'static, str>),
198    /// Only usable within written books. Changes the page of the book. Indexing
199    /// starts at 1.
200    ChangePage(i32),
201    /// Copies the given text to clipboard
202    CopyToClipboard(Cow<'static, str>),
203}
204
205/// Action to take when mouse-hovering on the text.
206#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
207#[serde(tag = "action", content = "contents", rename_all = "snake_case")]
208#[allow(clippy::enum_variant_names)]
209pub enum HoverEvent {
210    /// Displays a tooltip with the given text.
211    ShowText(Text),
212    /// Shows an item.
213    ShowItem {
214        /// Resource identifier of the item
215        id: Ident<Cow<'static, str>>,
216        /// Number of the items in the stack
217        count: Option<i32>,
218        /// NBT information about the item (sNBT format)
219        tag: Cow<'static, str>,
220    },
221    /// Shows an entity.
222    ShowEntity {
223        /// The entity's UUID
224        id: Uuid,
225        /// Resource identifier of the entity
226        #[serde(rename = "type")]
227        #[serde(default, skip_serializing_if = "Option::is_none")]
228        kind: Option<Ident<Cow<'static, str>>>,
229        /// Optional custom name for the entity
230        #[serde(default, skip_serializing_if = "Option::is_none")]
231        name: Option<Text>,
232    },
233}
234
235/// The font of the text.
236#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
237pub enum Font {
238    /// The default font.
239    #[serde(rename = "minecraft:default")]
240    Default,
241    /// Unicode font.
242    #[serde(rename = "minecraft:uniform")]
243    Uniform,
244    /// Enchanting table font.
245    #[serde(rename = "minecraft:alt")]
246    Alt,
247}
248
249#[allow(clippy::self_named_constructors)]
250impl Text {
251    /// Constructs a new plain text object.
252    pub fn text<P>(plain: P) -> Self
253    where
254        P: Into<Cow<'static, str>>,
255    {
256        Self(Box::new(TextInner {
257            content: TextContent::Text { text: plain.into() },
258            ..Default::default()
259        }))
260    }
261
262    /// Create translated text based on the given translation key, with extra
263    /// text components to be inserted into the slots of the translation text.
264    pub fn translate<K, W>(key: K, with: W) -> Self
265    where
266        K: Into<Cow<'static, str>>,
267        W: Into<Vec<Text>>,
268    {
269        Self(Box::new(TextInner {
270            content: TextContent::Translate {
271                translate: key.into(),
272                with: with.into(),
273            },
274            ..Default::default()
275        }))
276    }
277
278    /// Create a score from the scoreboard with an optional custom value.
279    pub fn score<N, O>(name: N, objective: O, value: Option<Cow<'static, str>>) -> Self
280    where
281        N: Into<Cow<'static, str>>,
282        O: Into<Cow<'static, str>>,
283    {
284        Self(Box::new(TextInner {
285            content: TextContent::ScoreboardValue {
286                score: ScoreboardValueContent {
287                    name: name.into(),
288                    objective: objective.into(),
289                    value,
290                },
291            },
292            ..Default::default()
293        }))
294    }
295
296    /// Creates a text component for selecting entity names with an optional
297    /// custom separator.
298    pub fn selector<S>(selector: S, separator: Option<Text>) -> Self
299    where
300        S: Into<Cow<'static, str>>,
301    {
302        Self(Box::new(TextInner {
303            content: TextContent::EntityNames {
304                selector: selector.into(),
305                separator,
306            },
307            ..Default::default()
308        }))
309    }
310
311    /// Creates a text component for a keybind. The keybind should be a valid
312    /// [`keybind identifier`].
313    ///
314    /// [`keybind identifier`]: https://minecraft.wiki/w/Controls#Configurable_controls
315    pub fn keybind<K>(keybind: K) -> Self
316    where
317        K: Into<Cow<'static, str>>,
318    {
319        Self(Box::new(TextInner {
320            content: TextContent::Keybind {
321                keybind: keybind.into(),
322            },
323            ..Default::default()
324        }))
325    }
326
327    /// Creates a text component for a block NBT tag.
328    pub fn block_nbt<B, N>(
329        block: B,
330        nbt: N,
331        interpret: Option<bool>,
332        separator: Option<Text>,
333    ) -> Self
334    where
335        B: Into<Cow<'static, str>>,
336        N: Into<Cow<'static, str>>,
337    {
338        Self(Box::new(TextInner {
339            content: TextContent::BlockNbt {
340                block: block.into(),
341                nbt: nbt.into(),
342                interpret,
343                separator,
344            },
345            ..Default::default()
346        }))
347    }
348
349    /// Creates a text component for an entity NBT tag.
350    pub fn entity_nbt<E, N>(
351        entity: E,
352        nbt: N,
353        interpret: Option<bool>,
354        separator: Option<Text>,
355    ) -> Self
356    where
357        E: Into<Cow<'static, str>>,
358        N: Into<Cow<'static, str>>,
359    {
360        Self(Box::new(TextInner {
361            content: TextContent::EntityNbt {
362                entity: entity.into(),
363                nbt: nbt.into(),
364                interpret,
365                separator,
366            },
367            ..Default::default()
368        }))
369    }
370
371    /// Creates a text component for a command storage NBT tag.
372    pub fn storage_nbt<S, N>(
373        storage: S,
374        nbt: N,
375        interpret: Option<bool>,
376        separator: Option<Text>,
377    ) -> Self
378    where
379        S: Into<Ident<Cow<'static, str>>>,
380        N: Into<Cow<'static, str>>,
381    {
382        Self(Box::new(TextInner {
383            content: TextContent::StorageNbt {
384                storage: storage.into(),
385                nbt: nbt.into(),
386                interpret,
387                separator,
388            },
389            ..Default::default()
390        }))
391    }
392
393    /// Returns `true` if the text contains no characters. Returns `false`
394    /// otherwise.
395    pub fn is_empty(&self) -> bool {
396        for extra in &self.0.extra {
397            if !extra.is_empty() {
398                return false;
399            }
400        }
401
402        match &self.0.content {
403            TextContent::Text { text } => text.is_empty(),
404            TextContent::Translate { translate, .. } => translate.is_empty(),
405            TextContent::ScoreboardValue { score } => {
406                let ScoreboardValueContent {
407                    name, objective, ..
408                } = score;
409
410                name.is_empty() || objective.is_empty()
411            }
412            TextContent::EntityNames { selector, .. } => selector.is_empty(),
413            TextContent::Keybind { keybind } => keybind.is_empty(),
414            TextContent::BlockNbt { nbt, .. } => nbt.is_empty(),
415            TextContent::EntityNbt { nbt, .. } => nbt.is_empty(),
416            TextContent::StorageNbt { nbt, .. } => nbt.is_empty(),
417        }
418    }
419
420    /// Converts the [`Text`] object to a plain string with the [legacy formatting (`§` and format codes)](https://wiki.vg/Chat#Old_system)
421    ///
422    /// Removes everything that can't be represented with a `§` and a modifier.
423    /// Any colors not on the [the legacy color list](https://wiki.vg/Chat#Colors) will be replaced with their closest equivalent.
424    pub fn to_legacy_lossy(&self) -> String {
425        // For keeping track of the currently active modifiers
426        #[derive(Default, Clone)]
427        struct Modifiers {
428            obfuscated: Option<bool>,
429            bold: Option<bool>,
430            strikethrough: Option<bool>,
431            underlined: Option<bool>,
432            italic: Option<bool>,
433            color: Option<Color>,
434        }
435
436        impl Modifiers {
437            // Writes all active modifiers to a String as `§<mod>`
438            fn write(&self, output: &mut String) {
439                if let Some(color) = self.color {
440                    let code = match color {
441                        Color::Rgb(rgb) => rgb.to_named_lossy().hex_digit(),
442                        Color::Named(normal) => normal.hex_digit(),
443                        Color::Reset => return,
444                    };
445
446                    output.push('§');
447                    output.push(code);
448                }
449                if let Some(true) = self.obfuscated {
450                    output.push_str("§k");
451                }
452                if let Some(true) = self.bold {
453                    output.push_str("§l");
454                }
455                if let Some(true) = self.strikethrough {
456                    output.push_str("§m");
457                }
458                if let Some(true) = self.underlined {
459                    output.push_str("§n");
460                }
461                if let Some(true) = self.italic {
462                    output.push_str("§o");
463                }
464            }
465            // Merges 2 Modifiers. The result is what you would get if you applied them both
466            // sequentially.
467            fn add(&self, other: &Self) -> Self {
468                Self {
469                    obfuscated: other.obfuscated.or(self.obfuscated),
470                    bold: other.bold.or(self.bold),
471                    strikethrough: other.strikethrough.or(self.strikethrough),
472                    underlined: other.underlined.or(self.underlined),
473                    italic: other.italic.or(self.italic),
474                    color: other.color.or(self.color),
475                }
476            }
477        }
478
479        fn to_legacy_inner(this: &Text, result: &mut String, mods: &mut Modifiers) {
480            let new_mods = Modifiers {
481                obfuscated: this.0.obfuscated,
482                bold: this.0.bold,
483                strikethrough: this.0.strikethrough,
484                underlined: this.0.underlined,
485                italic: this.0.italic,
486                color: this.0.color,
487            };
488
489            // If any modifiers were removed
490            if [
491                this.0.obfuscated,
492                this.0.bold,
493                this.0.strikethrough,
494                this.0.underlined,
495                this.0.italic,
496            ]
497            .contains(&Some(false))
498                || this.0.color == Some(Color::Reset)
499            {
500                // Reset and print sum of old and new modifiers
501                result.push_str("§r");
502                mods.add(&new_mods).write(result);
503            } else {
504                // Print only new modifiers
505                new_mods.write(result);
506            }
507
508            *mods = mods.add(&new_mods);
509
510            if let TextContent::Text { text } = &this.0.content {
511                result.push_str(text);
512            }
513
514            for child in &this.0.extra {
515                to_legacy_inner(child, result, mods);
516            }
517        }
518
519        let mut result = String::new();
520        let mut mods = Modifiers::default();
521        to_legacy_inner(self, &mut result, &mut mods);
522
523        result
524    }
525}
526
527impl Deref for Text {
528    type Target = TextInner;
529
530    fn deref(&self) -> &Self::Target {
531        &self.0
532    }
533}
534
535impl DerefMut for Text {
536    fn deref_mut(&mut self) -> &mut Self::Target {
537        &mut self.0
538    }
539}
540
541impl<T: IntoText<'static>> ops::Add<T> for Text {
542    type Output = Self;
543
544    fn add(self, rhs: T) -> Self::Output {
545        self.add_child(rhs)
546    }
547}
548
549impl<T: IntoText<'static>> ops::AddAssign<T> for Text {
550    fn add_assign(&mut self, rhs: T) {
551        self.extra.push(rhs.into_text());
552    }
553}
554
555impl From<Text> for Cow<'_, Text> {
556    fn from(value: Text) -> Self {
557        Cow::Owned(value)
558    }
559}
560
561impl<'a> From<&'a Text> for Cow<'a, Text> {
562    fn from(value: &'a Text) -> Self {
563        Cow::Borrowed(value)
564    }
565}
566
567impl FromStr for Text {
568    type Err = serde_json::error::Error;
569
570    fn from_str(s: &str) -> Result<Self, Self::Err> {
571        if s.is_empty() {
572            Ok(Text::default())
573        } else {
574            serde_json::from_str(s)
575        }
576    }
577}
578
579impl From<Text> for String {
580    fn from(value: Text) -> Self {
581        format!("{value}")
582    }
583}
584
585impl From<Text> for Value {
586    fn from(value: Text) -> Self {
587        Value::String(value.into())
588    }
589}
590
591impl fmt::Debug for Text {
592    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
593        fmt::Display::fmt(self, f)
594    }
595}
596
597impl fmt::Display for Text {
598    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
599        let string = if f.alternate() {
600            serde_json::to_string_pretty(self)
601        } else {
602            serde_json::to_string(self)
603        }
604        .map_err(|_| fmt::Error)?;
605
606        f.write_str(&string)
607    }
608}
609
610impl Default for TextContent {
611    fn default() -> Self {
612        Self::Text { text: "".into() }
613    }
614}
615
616impl<'de> Deserialize<'de> for Text {
617    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
618        struct TextVisitor;
619
620        impl<'de> Visitor<'de> for TextVisitor {
621            type Value = Text;
622
623            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
624                write!(formatter, "a text component data type")
625            }
626
627            fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
628                Ok(Text::text(v.to_string()))
629            }
630
631            fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
632                Ok(Text::text(v.to_string()))
633            }
634
635            fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
636                Ok(Text::text(v.to_string()))
637            }
638
639            fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
640                Ok(Text::text(v.to_string()))
641            }
642
643            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
644                Ok(Text::text(v.to_owned()))
645            }
646
647            fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
648                Ok(Text::text(v))
649            }
650
651            fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
652                let Some(mut res) = seq.next_element()? else {
653                    return Ok(Text::default());
654                };
655
656                while let Some(child) = seq.next_element::<Text>()? {
657                    res += child;
658                }
659
660                Ok(res)
661            }
662
663            fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
664                use de::value::MapAccessDeserializer;
665
666                Ok(Text(Box::new(TextInner::deserialize(
667                    MapAccessDeserializer::new(map),
668                )?)))
669            }
670        }
671
672        deserializer.deserialize_any(TextVisitor)
673    }
674}