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;
19pub use color::Color;
22pub use into_text::IntoText;
23
24#[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)]
62pub struct JsonText(pub Text);
65
66#[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#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
108#[serde(untagged)]
109pub enum TextContent {
110 Text {
112 #[serde(deserialize_with = "deserialize_cow_str_from_any")]
113 #[serde(alias = "")]
114 text: Cow<'static, str>,
115 },
116 Translate {
120 translate: Cow<'static, str>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 #[serde(alias = "")]
126 fallback: Option<Cow<'static, str>>,
127 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 with: Vec<Text>,
131 },
132 ScoreboardValue { score: ScoreboardValueContent },
134 EntityNames {
138 selector: Cow<'static, str>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
145 separator: Option<Text>,
146 },
147 Keybind {
150 keybind: Cow<'static, str>,
155 },
156 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 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 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#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
187pub struct ScoreboardValueContent {
188 pub name: Cow<'static, str>,
193 pub objective: Cow<'static, str>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub value: Option<Cow<'static, str>>,
199}
200
201#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
203#[serde(tag = "action", rename_all = "snake_case")]
204pub enum ClickEvent {
205 OpenUrl { url: Cow<'static, str> },
207 OpenFile { path: Cow<'static, str> },
209 RunCommand { command: Cow<'static, str> },
212 SuggestCommand { command: Cow<'static, str> },
215 ChangePage { page: i32 },
218 CopyToClipboard { value: Cow<'static, str> },
220}
221
222#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
223#[serde(transparent)]
224pub struct NBTUuid([i32; 4]); impl 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#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
256#[serde(tag = "action", rename_all = "snake_case")]
257#[allow(clippy::enum_variant_names)]
258pub enum HoverEvent {
259 ShowText {
261 #[serde(alias = "contents", alias = "text")]
262 value: Text,
263 },
264 ShowItem {
266 id: Ident<Cow<'static, str>>,
268 count: Option<i32>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 components: Option<Cow<'static, Compound>>, },
275 ShowEntity {
277 uuid: NBTUuid,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 id: Option<Ident<Cow<'static, str>>>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 name: Option<Text>,
285 },
286}
287
288#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
290pub enum Font {
291 #[serde(rename = "minecraft:default")]
293 Default,
294 #[serde(rename = "minecraft:uniform")]
296 Uniform,
297 #[serde(rename = "minecraft:alt")]
299 Alt,
300}
301
302#[allow(clippy::self_named_constructors)]
303impl Text {
304 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 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 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 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 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 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 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 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 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 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 pub fn to_legacy_lossy(&self) -> String {
496 #[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 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 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 [
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 result.push_str("§r");
573 mods.add(&new_mods).write(result);
574 } else {
575 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 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}