1use std::{
2 fmt::{self, Display},
3 io::{self, Cursor, Write},
4 sync::LazyLock,
5};
6
7#[cfg(feature = "azalea-buf")]
8use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
9use serde::{Deserialize, Deserializer, Serialize, de};
10#[cfg(feature = "simdnbt")]
11use simdnbt::{Deserialize as _, FromNbtTag as _, Serialize as _};
12use tracing::{debug, trace, warn};
13
14use crate::{
15 base_component::BaseComponent,
16 style::{ChatFormatting, Style},
17 text_component::TextComponent,
18 translatable_component::{StringOrComponent, TranslatableComponent},
19};
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
23#[serde(untagged)]
24pub enum FormattedText {
25 Text(TextComponent),
26 Translatable(TranslatableComponent),
27}
28
29pub static DEFAULT_STYLE: LazyLock<Style> = LazyLock::new(|| Style {
30 color: Some(ChatFormatting::White.try_into().unwrap()),
31 ..Style::default()
32});
33
34impl FormattedText {
36 pub fn get_base_mut(&mut self) -> &mut BaseComponent {
37 match self {
38 Self::Text(c) => &mut c.base,
39 Self::Translatable(c) => &mut c.base,
40 }
41 }
42
43 pub fn get_base(&self) -> &BaseComponent {
44 match self {
45 Self::Text(c) => &c.base,
46 Self::Translatable(c) => &c.base,
47 }
48 }
49
50 fn append(&mut self, sibling: FormattedText) {
52 self.get_base_mut().siblings.push(sibling);
53 }
54
55 fn parse_separator(
57 json: &serde_json::Value,
58 ) -> Result<Option<FormattedText>, serde_json::Error> {
59 if let Some(separator) = json.get("separator") {
60 return Ok(Some(FormattedText::deserialize(separator)?));
61 }
62 Ok(None)
63 }
64
65 #[cfg(feature = "simdnbt")]
66 fn parse_separator_nbt(nbt: &simdnbt::borrow::NbtCompound) -> Option<FormattedText> {
67 if let Some(separator) = nbt.get("separator") {
68 FormattedText::from_nbt_tag(separator)
69 } else {
70 None
71 }
72 }
73
74 pub fn to_custom_format<F, S, C>(
114 &self,
115 mut style_formatter: F,
116 mut text_formatter: S,
117 mut cleanup_formatter: C,
118 default_style: &Style,
119 ) -> String
120 where
121 F: FnMut(&Style, &Style, &Style) -> (String, String),
122 S: FnMut(&str) -> String,
123 C: FnMut(&Style) -> String,
124 {
125 let mut output = String::new();
126 let mut running_style = Style::default();
127
128 for component in self.clone().into_iter() {
129 let component_text = match &component {
130 Self::Text(c) => c.text.to_string(),
131 Self::Translatable(c) => match c.read() {
132 Ok(c) => c.to_string(),
133 Err(_) => c.key.to_string(),
134 },
135 };
136
137 let component_style = &component.get_base().style;
138
139 let formatted_style = style_formatter(&running_style, component_style, default_style);
140 let formatted_text = text_formatter(&component_text);
141
142 output.push_str(&formatted_style.0);
143 output.push_str(&formatted_text);
144 output.push_str(&formatted_style.1);
145
146 if component_style.reset {
148 running_style = default_style.clone();
149 } else {
150 running_style.apply(component_style);
151 }
152 }
153
154 output.push_str(&cleanup_formatter(&running_style));
155
156 output
157 }
158
159 pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
165 self.to_custom_format(
166 |running, new, default| (running.compare_ansi(new, default), "".to_owned()),
167 |text| text.to_string(),
168 |style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(),
169 default_style,
170 )
171 }
172
173 pub fn to_ansi(&self) -> String {
195 self.to_ansi_with_custom_style(&DEFAULT_STYLE)
196 }
197
198 pub fn to_html(&self) -> String {
199 self.to_custom_format(
200 |running, new, _| {
201 (
202 format!(
203 "<span style=\"{}\">",
204 running.merged_with(new).get_html_style()
205 ),
206 "</span>".to_owned(),
207 )
208 },
209 |text| {
210 text.replace("&", "&")
211 .replace("<", "<")
212 .replace(">", ">")
214 .replace("\n", "<br>")
215 },
216 |_| "".to_string(),
217 &DEFAULT_STYLE,
218 )
219 }
220}
221
222impl IntoIterator for FormattedText {
223 fn into_iter(self) -> Self::IntoIter {
225 let base = self.get_base();
226 let siblings = base.siblings.clone();
227 let mut v: Vec<FormattedText> = Vec::with_capacity(siblings.len() + 1);
228 v.push(self);
229 for sibling in siblings {
230 v.extend(sibling);
231 }
232
233 v.into_iter()
234 }
235
236 type Item = FormattedText;
237 type IntoIter = std::vec::IntoIter<Self::Item>;
238}
239
240impl<'de> Deserialize<'de> for FormattedText {
241 fn deserialize<D>(de: D) -> Result<Self, D::Error>
242 where
243 D: Deserializer<'de>,
244 {
245 let json: serde_json::Value = serde::Deserialize::deserialize(de)?;
246
247 let mut component: FormattedText;
249
250 if !json.is_array() && !json.is_object() {
252 return Ok(FormattedText::Text(TextComponent::new(
253 json.as_str().unwrap_or("").to_string(),
254 )));
255 }
256 else if json.is_object() {
258 if let Some(text) = json.get("text") {
259 let text = text.as_str().unwrap_or("").to_string();
260 component = FormattedText::Text(TextComponent::new(text));
261 } else if let Some(translate) = json.get("translate") {
262 let translate = translate
263 .as_str()
264 .ok_or_else(|| de::Error::custom("\"translate\" must be a string"))?
265 .into();
266 if let Some(with) = json.get("with") {
267 let with = with
268 .as_array()
269 .ok_or_else(|| de::Error::custom("\"with\" must be an array"))?;
270 let mut with_array = Vec::with_capacity(with.len());
271 for item in with {
272 let c = FormattedText::deserialize(item).map_err(de::Error::custom)?;
276 if let FormattedText::Text(text_component) = c
277 && text_component.base.siblings.is_empty()
278 && text_component.base.style.is_empty()
279 {
280 with_array.push(StringOrComponent::String(text_component.text));
281 continue;
282 }
283 with_array.push(StringOrComponent::FormattedText(
284 FormattedText::deserialize(item).map_err(de::Error::custom)?,
285 ));
286 }
287 component = FormattedText::Translatable(TranslatableComponent::new(
288 translate, with_array,
289 ));
290 } else {
291 component = FormattedText::Translatable(TranslatableComponent::new(
293 translate,
294 Vec::new(),
295 ));
296 }
297 } else if let Some(score) = json.get("score") {
298 if score.get("name").is_none() || score.get("objective").is_none() {
300 return Err(de::Error::missing_field(
301 "A score component needs at least a name and an objective",
302 ));
303 }
304 return Err(de::Error::custom(
306 "score text components aren't yet supported",
307 ));
308 } else if json.get("selector").is_some() {
309 return Err(de::Error::custom(
310 "selector text components aren't yet supported",
311 ));
312 } else if json.get("keybind").is_some() {
313 return Err(de::Error::custom(
314 "keybind text components aren't yet supported",
315 ));
316 } else {
317 let Some(_nbt) = json.get("nbt") else {
318 return Err(de::Error::custom(
319 format!("Don't know how to turn {json} into a FormattedText").as_str(),
320 ));
321 };
322 let _separator =
323 FormattedText::parse_separator(&json).map_err(de::Error::custom)?;
324
325 let _interpret = match json.get("interpret") {
326 Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
327 None => false,
328 };
329 if let Some(_block) = json.get("block") {}
330 return Err(de::Error::custom(
331 "nbt text components aren't yet supported",
332 ));
333 }
334 if let Some(extra) = json.get("extra") {
335 let Some(extra) = extra.as_array() else {
336 return Err(de::Error::custom("Extra isn't an array"));
337 };
338 if extra.is_empty() {
339 return Err(de::Error::custom("Unexpected empty array of components"));
340 }
341 for extra_component in extra {
342 let sibling =
343 FormattedText::deserialize(extra_component).map_err(de::Error::custom)?;
344 component.append(sibling);
345 }
346 }
347
348 let style = Style::deserialize(&json);
349 component.get_base_mut().style = style;
350
351 return Ok(component);
352 }
353 else if !json.is_array() {
355 return Err(de::Error::custom(
356 format!("Don't know how to turn {json} into a FormattedText").as_str(),
357 ));
358 }
359 let json_array = json.as_array().unwrap();
360 let mut component =
363 FormattedText::deserialize(&json_array[0]).map_err(de::Error::custom)?;
364 for i in 1..json_array.len() {
365 component.append(
366 FormattedText::deserialize(json_array.get(i).unwrap())
367 .map_err(de::Error::custom)?,
368 );
369 }
370 Ok(component)
371 }
372}
373
374#[cfg(feature = "simdnbt")]
375impl simdnbt::Serialize for FormattedText {
376 fn to_compound(self) -> simdnbt::owned::NbtCompound {
377 match self {
378 FormattedText::Text(c) => c.to_compound(),
379 FormattedText::Translatable(c) => c.to_compound(),
380 }
381 }
382}
383
384#[cfg(feature = "simdnbt")]
385impl simdnbt::FromNbtTag for FormattedText {
386 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
387 if let Some(string) = tag.string() {
389 Some(FormattedText::from(string))
390 }
391 else if let Some(compound) = tag.compound() {
394 FormattedText::from_nbt_compound(compound)
395 }
396 else if let Some(list) = tag.list() {
398 let mut component;
399 if let Some(compounds) = list.compounds() {
400 component = FormattedText::from_nbt_compound(compounds.first()?)?;
401 for compound in compounds.into_iter().skip(1) {
402 component.append(FormattedText::from_nbt_compound(compound)?);
403 }
404 } else if let Some(strings) = list.strings() {
405 component = FormattedText::from(*(strings.first()?));
406 for &string in strings.iter().skip(1) {
407 component.append(FormattedText::from(string));
408 }
409 } else {
410 debug!("couldn't parse {list:?} as FormattedText");
411 return None;
412 }
413 Some(component)
414 } else {
415 Some(FormattedText::Text(TextComponent::new("".to_owned())))
416 }
417 }
418}
419
420#[cfg(feature = "simdnbt")]
421impl FormattedText {
422 pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
423 let mut component: FormattedText;
424
425 if let Some(text) = compound.get("text") {
426 let text = text.string().unwrap_or_default().to_string();
427 component = FormattedText::Text(TextComponent::new(text));
428 } else if let Some(translate) = compound.get("translate") {
429 let translate = translate.string()?.into();
430 if let Some(with) = compound.get("with") {
431 let mut with_array = Vec::new();
432 let with_list = with.list()?;
433 if with_list.empty() {
434 } else if let Some(with) = with_list.strings() {
435 for item in with {
436 with_array.push(StringOrComponent::String(item.to_string()));
437 }
438 } else if let Some(with) = with_list.compounds() {
439 for item in with {
440 if let Some(primitive) = item.get("") {
445 if let Some(b) = primitive.byte() {
448 with_array.push(StringOrComponent::String(
450 if b != 0 { "true" } else { "false" }.to_string(),
451 ));
452 } else if let Some(s) = primitive.short() {
453 with_array.push(StringOrComponent::String(s.to_string()));
454 } else if let Some(i) = primitive.int() {
455 with_array.push(StringOrComponent::String(i.to_string()));
456 } else if let Some(l) = primitive.long() {
457 with_array.push(StringOrComponent::String(l.to_string()));
458 } else if let Some(f) = primitive.float() {
459 with_array.push(StringOrComponent::String(f.to_string()));
460 } else if let Some(d) = primitive.double() {
461 with_array.push(StringOrComponent::String(d.to_string()));
462 } else if let Some(s) = primitive.string() {
463 with_array.push(StringOrComponent::String(s.to_string()));
464 } else {
465 warn!(
466 "couldn't parse {item:?} as FormattedText because it has a disallowed primitive"
467 );
468 with_array.push(StringOrComponent::String("?".to_string()));
469 }
470 } else if let Some(c) = FormattedText::from_nbt_compound(item) {
471 if let FormattedText::Text(text_component) = c
472 && text_component.base.siblings.is_empty()
473 && text_component.base.style.is_empty()
474 {
475 with_array.push(StringOrComponent::String(text_component.text));
476 continue;
477 }
478 with_array.push(StringOrComponent::FormattedText(
479 FormattedText::from_nbt_compound(item)?,
480 ));
481 } else {
482 warn!("couldn't parse {item:?} as FormattedText");
483 with_array.push(StringOrComponent::String("?".to_string()));
484 }
485 }
486 } else {
487 warn!(
488 "couldn't parse {with:?} as FormattedText because it's not a list of compounds"
489 );
490 return None;
491 }
492 component =
493 FormattedText::Translatable(TranslatableComponent::new(translate, with_array));
494 } else {
495 component =
497 FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
498 }
499 } else if let Some(score) = compound.compound("score") {
500 if score.get("name").is_none() || score.get("objective").is_none() {
502 trace!("A score component needs at least a name and an objective");
504 return None;
505 }
506 return None;
508 } else if compound.get("selector").is_some() {
509 trace!("selector text components aren't yet supported");
511 return None;
512 } else if compound.get("keybind").is_some() {
513 trace!("keybind text components aren't yet supported");
515 return None;
516 } else if let Some(tag) = compound.get("") {
517 return FormattedText::from_nbt_tag(tag);
518 } else {
519 let _nbt = compound.get("nbt")?;
520 let _separator = FormattedText::parse_separator_nbt(&compound)?;
521
522 let _interpret = match compound.get("interpret") {
523 Some(v) => v.byte().unwrap_or_default() != 0,
524 None => false,
525 };
526 if let Some(_block) = compound.get("block") {}
527 trace!("nbt text components aren't yet supported");
528 return None;
529 }
530 if let Some(extra) = compound.get("extra") {
531 component.append(FormattedText::from_nbt_tag(extra)?);
532 }
533
534 let base_style = Style::from_compound(compound).ok()?;
535 let new_style = &mut component.get_base_mut().style;
536 *new_style = new_style.merged_with(&base_style);
537
538 Some(component)
539 }
540}
541
542#[cfg(feature = "simdnbt")]
543impl From<&simdnbt::Mutf8Str> for FormattedText {
544 fn from(s: &simdnbt::Mutf8Str) -> Self {
545 FormattedText::Text(TextComponent::new(s.to_string()))
546 }
547}
548
549#[cfg(feature = "azalea-buf")]
550#[cfg(feature = "simdnbt")]
551impl AzaleaRead for FormattedText {
552 fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
553 let nbt = simdnbt::borrow::read_optional_tag(buf)?;
554 match nbt {
555 Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
556 "couldn't convert nbt to chat message".to_owned(),
557 )),
558 _ => Ok(FormattedText::default()),
559 }
560 }
561}
562
563#[cfg(feature = "azalea-buf")]
564#[cfg(feature = "simdnbt")]
565impl AzaleaWrite for FormattedText {
566 fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
567 let mut out = Vec::new();
568 simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
569 buf.write_all(&out)
570 }
571}
572
573impl From<String> for FormattedText {
574 fn from(s: String) -> Self {
575 FormattedText::Text(TextComponent {
576 text: s,
577 base: BaseComponent::default(),
578 })
579 }
580}
581impl From<&str> for FormattedText {
582 fn from(s: &str) -> Self {
583 Self::from(s.to_string())
584 }
585}
586
587impl Display for FormattedText {
588 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
589 match self {
590 FormattedText::Text(c) => c.fmt(f),
591 FormattedText::Translatable(c) => c.fmt(f),
592 }
593 }
594}
595
596impl Default for FormattedText {
597 fn default() -> Self {
598 FormattedText::Text(TextComponent::default())
599 }
600}