fix: from_legacy_string

This commit is contained in:
Alexander Medvedev 2026-07-26 10:25:13 +02:00
parent faf8c50d80
commit ac89bff1ca
3 changed files with 293 additions and 112 deletions

1
.gitignore vendored
View File

@ -153,3 +153,4 @@ spigot.yml
version_history.json
world_nether/
world_the_end/
patchbukkit/

View File

@ -0,0 +1,291 @@
use std::fmt::Write;
use crate::text::color::Color;
use crate::text::{TextComponent, TextContent};
use crate::translation::{Locale, get_translation_text};
impl TextComponent {
/// Parses a legacy Minecraft formatted string (using section signs '§' by default) into a text component.
///
/// Legacy formatting uses formatting codes after a color symbol:
/// - Colors: 0-9, a-f
/// - Styles: l (bold), o (italic), n (underline), m (strikethrough), k (obfuscated)
/// - Reset: r
/// - RGB hex colors: §x§R§R§G§G§B§B
///
/// # Arguments
/// - `input` – The legacy formatted string.
///
/// # Returns
/// A `TextComponent` with the parsed formatting applied.
#[must_use]
pub fn from_legacy_string(input: &str) -> Self {
Self::from_legacy_string_with_code(input, '§')
}
/// Parses a legacy formatted string using a custom color code symbol (e.g. '&' or '§').
///
/// # Arguments
/// - `input` – The legacy formatted string.
/// - `code_symbol` – The delimiter character (e.g. `'§'` or `'&'`).
///
/// # Returns
/// A `TextComponent` with the parsed formatting applied.
#[must_use]
#[expect(clippy::too_many_lines)]
pub fn from_legacy_string_with_code(input: &str, code_symbol: char) -> Self {
let mut root = Self::text("");
let mut parts = input.split(code_symbol);
if let Some(first) = parts.next()
&& !first.is_empty()
{
root = root.add_child(Self::text(first.to_string()));
}
let mut current_color: Option<Color> = None;
let mut bold = false;
let mut italic = false;
let mut underlined = false;
let mut strikethrough = false;
let mut obfuscated = false;
while let Some(part) = parts.next() {
if part.is_empty() {
continue;
}
let code = part.chars().next().unwrap_or(' ').to_ascii_lowercase();
let remainder = &part[code.len_utf8()..];
match code {
'x' => {
let mut hex = [0u8; 6];
let mut valid_hex = true;
let mut trailing_remainder = String::new();
for hex_byte in &mut hex {
if let Some(next_part) = parts.next() {
if let Some(c) = next_part.chars().next() {
*hex_byte = c as u8;
trailing_remainder.push_str(&next_part[c.len_utf8()..]);
} else {
valid_hex = false;
break;
}
} else {
valid_hex = false;
break;
}
}
if valid_hex && let Ok(hex_str) = std::str::from_utf8(&hex) {
current_color = Color::from_hex_str(hex_str);
}
if !remainder.is_empty() || !trailing_remainder.is_empty() {
let text = format!("{remainder}{trailing_remainder}");
if !text.is_empty() {
let mut child = Self::text(text);
if let Some(c) = current_color {
child = child.color(c);
}
if bold {
child = child.bold();
}
if italic {
child = child.italic();
}
if underlined {
child = child.underlined();
}
if strikethrough {
child = child.strikethrough();
}
if obfuscated {
child = child.obfuscated();
}
root = root.add_child(child);
}
}
continue;
}
'0'..='9' | 'a'..='f' => {
current_color = Color::from_legacy_code(code);
bold = false;
italic = false;
underlined = false;
strikethrough = false;
obfuscated = false;
}
'l' => bold = true,
'o' => italic = true,
'n' => underlined = true,
'm' => strikethrough = true,
'k' => obfuscated = true,
'r' => {
current_color = None;
bold = false;
italic = false;
underlined = false;
strikethrough = false;
obfuscated = false;
}
_ => {}
}
if !remainder.is_empty() {
let mut child = Self::text(remainder.to_string());
if let Some(c) = current_color {
child = child.color(c);
}
if bold {
child = child.bold();
}
if italic {
child = child.italic();
}
if underlined {
child = child.underlined();
}
if strikethrough {
child = child.strikethrough();
}
if obfuscated {
child = child.obfuscated();
}
root = root.add_child(child);
}
}
root
}
/// Serializes this component into a Java legacy formatted string using `§`.
///
/// # Arguments
/// - `locale` – The locale to use for resolving translation components.
///
/// # Returns
/// A legacy Minecraft formatted string with `§` color and style codes.
#[must_use]
pub fn to_legacy_string(&self, locale: Locale) -> String {
self.to_legacy_string_with_code('§', locale)
}
/// Serializes this component into a legacy formatted string using a custom color code symbol.
///
/// # Arguments
/// - `code_symbol` – The formatting character symbol (e.g., `'§'` or `'&'`).
/// - `locale` – The locale to use for resolving translation components.
///
/// # Returns
/// A legacy formatted string with color and style codes using `code_symbol`.
#[must_use]
pub fn to_legacy_string_with_code(&self, code_symbol: char, locale: Locale) -> String {
let mut text = String::new();
// 1. Color formatting
if let Some(color) = &self.0.style.color {
match color {
Color::Named(named) => {
let _ = write!(text, "{code_symbol}{}", named.to_legacy_char());
}
Color::Rgb(rgb) => {
let hex = format!("{:02x}{:02x}{:02x}", rgb.red, rgb.green, rgb.blue);
let _ = write!(text, "{code_symbol}x");
for ch in hex.chars() {
let _ = write!(text, "{code_symbol}{ch}");
}
}
Color::Reset => {
let _ = write!(text, "{code_symbol}r");
}
}
}
// 2. Style formatting
if self.0.style.bold == Some(true) {
let _ = write!(text, "{code_symbol}l");
}
if self.0.style.italic == Some(true) {
let _ = write!(text, "{code_symbol}o");
}
if self.0.style.underlined == Some(true) {
let _ = write!(text, "{code_symbol}n");
}
if self.0.style.strikethrough == Some(true) {
let _ = write!(text, "{code_symbol}m");
}
if self.0.style.obfuscated == Some(true) {
let _ = write!(text, "{code_symbol}k");
}
// 3. Resolve Content
match &*self.0.content {
TextContent::Text { text: t } => text.push_str(t),
TextContent::Translate {
translate,
bedrock_translate: _,
with,
} => {
text.push_str(&get_translation_text(
format!("minecraft:{translate}"),
locale,
with.clone(),
));
}
TextContent::EntityNames { selector, .. } => text.push_str(selector),
TextContent::Keybind { keybind } => text.push_str(keybind),
TextContent::Custom { key, with, .. } => {
text.push_str(&get_translation_text(key.clone(), locale, with.clone()));
}
}
// 4. Recursively append extra components
for child in &self.0.extra {
text.push_str(&Self(child.clone()).to_legacy_string_with_code(code_symbol, locale));
let _ = write!(text, "{code_symbol}r");
}
text
}
}
#[cfg(test)]
mod test {
use crate::text::TextComponent;
use crate::text::color::{Color, NamedColor};
use crate::translation::Locale;
#[test]
fn from_legacy_string_hex() {
let comp = TextComponent::from_legacy_string("§x§f§c§0§0§0§0Test String ▽");
assert_eq!(comp.0.extra.len(), 1);
if let crate::text::TextContent::Text { text } = &*comp.0.extra[0].content {
assert_eq!(text, "Test String ▽");
} else {
panic!("Expected Text content");
}
assert_eq!(comp.0.extra[0].style.color, Color::from_hex_str("fc0000"));
}
#[test]
fn from_legacy_string_alt_code() {
let comp = TextComponent::from_legacy_string_with_code("&cRed &lBold", '&');
assert_eq!(comp.0.extra.len(), 2);
assert_eq!(
comp.0.extra[0].style.color,
Some(Color::Named(NamedColor::Red))
);
assert_eq!(comp.0.extra[1].style.bold, Some(true));
}
#[test]
fn to_legacy_string() {
let comp = TextComponent::text("Hello ").add_child(
TextComponent::text("World")
.color_named(NamedColor::Red)
.bold(),
);
let legacy = comp.to_legacy_string(Locale::EnUs);
assert_eq!(legacy, "Hello §c§lWorld§r");
}
}

View File

@ -19,6 +19,7 @@ use style::Style;
pub mod click;
pub mod color;
pub mod hover;
pub mod legacy;
pub mod style;
/// Represents a Minecraft chat component.
@ -525,118 +526,6 @@ impl TextComponent {
})
}
/// Parses a legacy Minecraft formatted string (using section signs '§') into a text component.
///
/// Legacy formatting uses the section sign (§) followed by a formatting code:
/// - Colors: 0-9, a-f
/// - Styles: l (bold), o (italic), n (underline), m (strikethrough), k (obfuscated)
/// - Reset: r
/// - RGB hex colors: §x§R§R§G§G§B§B
///
/// # Arguments
/// - `input` – The legacy formatted string.
///
/// # Returns
/// A `TextComponent` with the parsed formatting applied.
#[must_use]
pub fn from_legacy_string(input: &str) -> Self {
let mut root = Self::text("");
let mut parts = input.split('§');
if let Some(first) = parts.next()
&& !first.is_empty()
{
root = root.add_child(Self::text(first.to_string()));
}
let mut current_color: Option<Color> = None;
let mut bold = false;
let mut italic = false;
let mut underlined = false;
let mut strikethrough = false;
let mut obfuscated = false;
while let Some(part) = parts.next() {
if part.is_empty() {
continue;
}
let code = part.chars().next().unwrap_or(' ').to_ascii_lowercase();
let remainder = &part[1..];
match code {
'x' => {
let mut hex = [0u8; 6];
let mut valid_hex = true;
for hex_byte in &mut hex {
if let Some(next_part) = parts.next() {
if let Some(c) = next_part.chars().next() {
*hex_byte = c as u8;
} else {
valid_hex = false;
break;
}
} else {
valid_hex = false;
break;
}
}
if valid_hex && let Ok(hex_str) = std::str::from_utf8(&hex) {
current_color = Color::from_hex_str(hex_str);
}
continue;
}
'0'..='9' | 'a'..='f' => {
current_color = Color::from_legacy_code(code);
bold = false;
italic = false;
underlined = false;
strikethrough = false;
obfuscated = false;
}
'l' => bold = true,
'o' => italic = true,
'n' => underlined = true,
'm' => strikethrough = true,
'k' => obfuscated = true,
'r' => {
current_color = None;
bold = false;
italic = false;
underlined = false;
strikethrough = false;
obfuscated = false;
}
_ => {}
}
if !remainder.is_empty() {
let mut child = Self::text(remainder.to_string());
if let Some(c) = current_color {
child = child.color(c);
}
if bold {
child = child.bold();
}
if italic {
child = child.italic();
}
if underlined {
child = child.underlined();
}
if strikethrough {
child = child.strikethrough();
}
if obfuscated {
child = child.obfuscated();
}
root = root.add_child(child);
}
}
root
}
/// Appends a child component to this component.
///
/// # Arguments