fix: from_legacy_string
This commit is contained in:
parent
faf8c50d80
commit
ac89bff1ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -153,3 +153,4 @@ spigot.yml
|
||||
version_history.json
|
||||
world_nether/
|
||||
world_the_end/
|
||||
patchbukkit/
|
||||
|
||||
291
pumpkin-util/src/text/legacy.rs
Normal file
291
pumpkin-util/src/text/legacy.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user