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#[derive(Clone, PartialEq, Default, Serialize)]
53#[serde(transparent)]
54pub struct Text(Box<TextInner>);
55
56#[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#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
99#[serde(untagged)]
100pub enum TextContent {
101 Text { text: Cow<'static, str> },
103 Translate {
107 translate: Cow<'static, str>,
110 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 with: Vec<Text>,
114 },
115 ScoreboardValue { score: ScoreboardValueContent },
117 EntityNames {
121 selector: Cow<'static, str>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
128 separator: Option<Text>,
129 },
130 Keybind {
133 keybind: Cow<'static, str>,
138 },
139 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 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 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#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
170pub struct ScoreboardValueContent {
171 pub name: Cow<'static, str>,
176 pub objective: Cow<'static, str>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub value: Option<Cow<'static, str>>,
182}
183
184#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
186#[serde(tag = "action", content = "value", rename_all = "snake_case")]
187pub enum ClickEvent {
188 OpenUrl(Cow<'static, str>),
190 OpenFile(Cow<'static, str>),
192 RunCommand(Cow<'static, str>),
195 SuggestCommand(Cow<'static, str>),
198 ChangePage(i32),
201 CopyToClipboard(Cow<'static, str>),
203}
204
205#[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 ShowText(Text),
212 ShowItem {
214 id: Ident<Cow<'static, str>>,
216 count: Option<i32>,
218 tag: Cow<'static, str>,
220 },
221 ShowEntity {
223 id: Uuid,
225 #[serde(rename = "type")]
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 kind: Option<Ident<Cow<'static, str>>>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 name: Option<Text>,
232 },
233}
234
235#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
237pub enum Font {
238 #[serde(rename = "minecraft:default")]
240 Default,
241 #[serde(rename = "minecraft:uniform")]
243 Uniform,
244 #[serde(rename = "minecraft:alt")]
246 Alt,
247}
248
249#[allow(clippy::self_named_constructors)]
250impl Text {
251 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 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 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 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 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 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 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 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 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 pub fn to_legacy_lossy(&self) -> String {
425 #[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 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 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 [
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 result.push_str("§r");
502 mods.add(&new_mods).write(result);
503 } else {
504 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}