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