mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
add legacy color codes
This commit is contained in:
parent
5039f9668f
commit
6026c74430
5 changed files with 186 additions and 53 deletions
|
@ -11,7 +11,7 @@ impl BaseComponent {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
siblings: Vec::new(),
|
siblings: Vec::new(),
|
||||||
style: Style::new(),
|
style: Style::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -11,8 +9,8 @@ use crate::{
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Component {
|
pub enum Component {
|
||||||
TextComponent(TextComponent),
|
Text(TextComponent),
|
||||||
TranslatableComponent(TranslatableComponent),
|
Translatable(TranslatableComponent),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A chat component
|
/// A chat component
|
||||||
|
@ -23,7 +21,7 @@ impl Component {
|
||||||
|
|
||||||
// if it's primitive, make it a text component
|
// if it's primitive, make it a text component
|
||||||
if !json.is_array() && !json.is_object() {
|
if !json.is_array() && !json.is_object() {
|
||||||
return Ok(Component::TextComponent(TextComponent::new(
|
return Ok(Component::Text(TextComponent::new(
|
||||||
json.as_str().unwrap_or("").to_string(),
|
json.as_str().unwrap_or("").to_string(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
@ -31,7 +29,7 @@ impl Component {
|
||||||
else if json.is_object() {
|
else if json.is_object() {
|
||||||
if json.get("text").is_some() {
|
if json.get("text").is_some() {
|
||||||
let text = json.get("text").unwrap().as_str().unwrap_or("").to_string();
|
let text = json.get("text").unwrap().as_str().unwrap_or("").to_string();
|
||||||
component = Component::TextComponent(TextComponent::new(text));
|
component = Component::Text(TextComponent::new(text));
|
||||||
} else if json.get("translate").is_some() {
|
} else if json.get("translate").is_some() {
|
||||||
let translate = json.get("translate").unwrap().to_string();
|
let translate = json.get("translate").unwrap().to_string();
|
||||||
if json.get("with").is_some() {
|
if json.get("with").is_some() {
|
||||||
|
@ -41,7 +39,7 @@ impl Component {
|
||||||
// if it's a string component with no styling and no siblings, just add a string to with_array
|
// if it's a string component with no styling and no siblings, just add a string to with_array
|
||||||
// otherwise add the component to the array
|
// otherwise add the component to the array
|
||||||
let c = Component::new(&with[i])?;
|
let c = Component::new(&with[i])?;
|
||||||
if let Component::TextComponent(text_component) = c {
|
if let Component::Text(text_component) = c {
|
||||||
if text_component.base.siblings.is_empty()
|
if text_component.base.siblings.is_empty()
|
||||||
&& text_component.base.style.is_empty()
|
&& text_component.base.style.is_empty()
|
||||||
{
|
{
|
||||||
|
@ -51,15 +49,12 @@ impl Component {
|
||||||
}
|
}
|
||||||
with_array.push(StringOrComponent::Component(Component::new(&with[i])?));
|
with_array.push(StringOrComponent::Component(Component::new(&with[i])?));
|
||||||
}
|
}
|
||||||
component = Component::TranslatableComponent(TranslatableComponent::new(
|
component =
|
||||||
translate, with_array,
|
Component::Translatable(TranslatableComponent::new(translate, with_array));
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
// if it doesn't have a "with", just have the with_array be empty
|
// if it doesn't have a "with", just have the with_array be empty
|
||||||
component = Component::TranslatableComponent(TranslatableComponent::new(
|
component =
|
||||||
translate,
|
Component::Translatable(TranslatableComponent::new(translate, Vec::new()));
|
||||||
Vec::new(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} else if json.get("score").is_some() {
|
} else if json.get("score").is_some() {
|
||||||
// object = GsonHelper.getAsJsonObject(jsonObject, "score");
|
// object = GsonHelper.getAsJsonObject(jsonObject, "score");
|
||||||
|
@ -157,15 +152,15 @@ impl Component {
|
||||||
|
|
||||||
pub fn get_base_mut(&mut self) -> &mut BaseComponent {
|
pub fn get_base_mut(&mut self) -> &mut BaseComponent {
|
||||||
match self {
|
match self {
|
||||||
Self::TextComponent(c) => &mut c.base,
|
Self::Text(c) => &mut c.base,
|
||||||
Self::TranslatableComponent(c) => &mut c.base,
|
Self::Translatable(c) => &mut c.base,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_base(&self) -> &BaseComponent {
|
pub fn get_base(&self) -> &BaseComponent {
|
||||||
match self {
|
match self {
|
||||||
Self::TextComponent(c) => &c.base,
|
Self::Text(c) => &c.base,
|
||||||
Self::TranslatableComponent(c) => &c.base,
|
Self::Translatable(c) => &c.base,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,30 +177,17 @@ impl Component {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively call the function for every component in this component
|
|
||||||
pub fn visit<F>(&self, f: &mut F)
|
|
||||||
where
|
|
||||||
// The closure takes an `i32` and returns an `i32`.
|
|
||||||
F: FnMut(&Component),
|
|
||||||
{
|
|
||||||
f(self);
|
|
||||||
self.get_base()
|
|
||||||
.siblings
|
|
||||||
.iter()
|
|
||||||
.for_each(|s| Component::visit(s, f));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert this component into an ansi string
|
/// Convert this component into an ansi string
|
||||||
pub fn to_ansi(&self) -> String {
|
pub fn to_ansi(&self) -> String {
|
||||||
// this contains the final string will all the ansi escape codes
|
// this contains the final string will all the ansi escape codes
|
||||||
let mut built_string = String::new();
|
let mut built_string = String::new();
|
||||||
// this style will update as we visit components
|
// this style will update as we visit components
|
||||||
let mut running_style = Style::new();
|
let mut running_style = Style::default();
|
||||||
|
|
||||||
self.visit(&mut |component| {
|
for component in self.clone().into_iter() {
|
||||||
let component_text = match component {
|
let component_text = match &component {
|
||||||
Self::TextComponent(c) => &c.text,
|
Self::Text(c) => &c.text,
|
||||||
Self::TranslatableComponent(c) => &c.key,
|
Self::Translatable(c) => &c.key,
|
||||||
};
|
};
|
||||||
let component_style = &component.get_base().style;
|
let component_style = &component.get_base().style;
|
||||||
|
|
||||||
|
@ -214,12 +196,30 @@ impl Component {
|
||||||
built_string.push_str(component_text);
|
built_string.push_str(component_text);
|
||||||
|
|
||||||
running_style.apply(component_style);
|
running_style.apply(component_style);
|
||||||
});
|
}
|
||||||
|
|
||||||
if !running_style.is_empty() {
|
if !running_style.is_empty() {
|
||||||
built_string.push_str("\x1b[m");
|
built_string.push_str("\u{1b}[m");
|
||||||
}
|
}
|
||||||
|
|
||||||
built_string
|
built_string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Component {
|
||||||
|
/// Recursively call the function for every component in this component
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
let base = self.get_base();
|
||||||
|
let siblings = base.siblings.clone();
|
||||||
|
let mut v: Vec<Component> = Vec::with_capacity(siblings.len() + 1);
|
||||||
|
v.push(self);
|
||||||
|
for sibling in siblings {
|
||||||
|
v.extend(sibling.into_iter());
|
||||||
|
}
|
||||||
|
|
||||||
|
v.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item = Component;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
}
|
||||||
|
|
|
@ -66,16 +66,16 @@ pub struct ChatFormatting<'a> {
|
||||||
|
|
||||||
pub struct Ansi {}
|
pub struct Ansi {}
|
||||||
impl Ansi {
|
impl Ansi {
|
||||||
pub const BOLD: &'static str = "\x1b[1m";
|
pub const BOLD: &'static str = "\u{1b}[1m";
|
||||||
pub const ITALIC: &'static str = "\x1b[3m";
|
pub const ITALIC: &'static str = "\u{1b}[3m";
|
||||||
pub const UNDERLINED: &'static str = "\x1b[4m";
|
pub const UNDERLINED: &'static str = "\u{1b}[4m";
|
||||||
pub const STRIKETHROUGH: &'static str = "\x1b[9m";
|
pub const STRIKETHROUGH: &'static str = "\u{1b}[9m";
|
||||||
pub const OBFUSCATED: &'static str = "\x1b[8m";
|
pub const OBFUSCATED: &'static str = "\u{1b}[8m";
|
||||||
pub const RESET: &'static str = "\x1b[m";
|
pub const RESET: &'static str = "\u{1b}[m";
|
||||||
|
|
||||||
pub fn rgb(value: u32) -> String {
|
pub fn rgb(value: u32) -> String {
|
||||||
format!(
|
format!(
|
||||||
"\x1b[38;2;{};{};{}m",
|
"\u{1b}[38;2;{};{};{}m",
|
||||||
(value >> 16) & 0xFF,
|
(value >> 16) & 0xFF,
|
||||||
(value >> 8) & 0xFF,
|
(value >> 8) & 0xFF,
|
||||||
value & 0xFF
|
value & 0xFF
|
||||||
|
@ -159,6 +159,15 @@ impl<'a> ChatFormatting<'a> {
|
||||||
color,
|
color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_code(code: char) -> Result<&'static ChatFormatting<'static>, String> {
|
||||||
|
for formatter in &ChatFormatting::FORMATTERS {
|
||||||
|
if formatter.code == code {
|
||||||
|
return Ok(formatter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(format!("Invalid formatting code {}", code))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextColor {
|
impl TextColor {
|
||||||
|
@ -212,7 +221,7 @@ pub struct Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Style {
|
impl Style {
|
||||||
pub fn new() -> Style {
|
pub fn default() -> Style {
|
||||||
Style {
|
Style {
|
||||||
color: None,
|
color: None,
|
||||||
bold: None,
|
bold: None,
|
||||||
|
@ -244,7 +253,7 @@ impl Style {
|
||||||
obfuscated,
|
obfuscated,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Style::new()
|
Style::default()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,7 +284,9 @@ impl Style {
|
||||||
} else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) {
|
} else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) {
|
||||||
true
|
true
|
||||||
// if it used to be obfuscated and now it's not, reset
|
// if it used to be obfuscated and now it's not, reset
|
||||||
} else { self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true) }
|
} else {
|
||||||
|
self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut ansi_codes = String::new();
|
let mut ansi_codes = String::new();
|
||||||
|
@ -287,7 +298,7 @@ impl Style {
|
||||||
ansi_codes.push_str(Ansi::RESET);
|
ansi_codes.push_str(Ansi::RESET);
|
||||||
let mut updated_after = self.clone();
|
let mut updated_after = self.clone();
|
||||||
updated_after.apply(after);
|
updated_after.apply(after);
|
||||||
(Style::new(), updated_after)
|
(Style::default(), updated_after)
|
||||||
} else {
|
} else {
|
||||||
(self.clone(), after.clone())
|
(self.clone(), after.clone())
|
||||||
};
|
};
|
||||||
|
@ -355,6 +366,37 @@ impl Style {
|
||||||
self.obfuscated = Some(*obfuscated);
|
self.obfuscated = Some(*obfuscated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a ChatFormatting to this style
|
||||||
|
pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
|
||||||
|
match formatting {
|
||||||
|
&ChatFormatting::BOLD => self.bold = Some(true),
|
||||||
|
&ChatFormatting::ITALIC => self.italic = Some(true),
|
||||||
|
&ChatFormatting::UNDERLINE => self.underlined = Some(true),
|
||||||
|
&ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true),
|
||||||
|
&ChatFormatting::OBFUSCATED => self.obfuscated = Some(true),
|
||||||
|
&ChatFormatting::RESET => {
|
||||||
|
self.color = None;
|
||||||
|
self.bold = None;
|
||||||
|
self.italic = None;
|
||||||
|
self.underlined = None;
|
||||||
|
self.strikethrough = None;
|
||||||
|
self.obfuscated = None;
|
||||||
|
}
|
||||||
|
&ChatFormatting {
|
||||||
|
name: _,
|
||||||
|
code: _,
|
||||||
|
is_format: _,
|
||||||
|
id: _,
|
||||||
|
color,
|
||||||
|
} => {
|
||||||
|
// if it's a color, set it
|
||||||
|
if let Some(color) = color {
|
||||||
|
self.color = Some(TextColor::from_rgb(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -422,4 +464,20 @@ mod tests {
|
||||||
let ansi_difference = style_a.compare_ansi(&style_b);
|
let ansi_difference = style_a.compare_ansi(&style_b);
|
||||||
assert_eq!(ansi_difference, Ansi::ITALIC)
|
assert_eq!(ansi_difference, Ansi::ITALIC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_code() {
|
||||||
|
assert_eq!(
|
||||||
|
ChatFormatting::from_code('a').unwrap(),
|
||||||
|
&ChatFormatting::GREEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_formatting() {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.apply_formatting(&ChatFormatting::BOLD);
|
||||||
|
style.apply_formatting(&ChatFormatting::RED);
|
||||||
|
assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::base_component::BaseComponent;
|
use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TextComponent {
|
pub struct TextComponent {
|
||||||
|
@ -6,6 +6,63 @@ pub struct TextComponent {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LEGACY_FORMATTING_CODE_SYMBOL: char = '§';
|
||||||
|
|
||||||
|
/// Convert a legacy color code string into a Component
|
||||||
|
/// Technically in Minecraft this is done when displaying the text, but AFAIK it's the same as just doing it in TextComponent
|
||||||
|
pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component {
|
||||||
|
let mut components: Vec<TextComponent> = Vec::with_capacity(1);
|
||||||
|
// iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL then read the next character and get the style from that
|
||||||
|
// otherwise, add the character to the text
|
||||||
|
|
||||||
|
// we don't use a normal for loop since we need to be able to skip after reading the formatter code symbol
|
||||||
|
let mut i = 0;
|
||||||
|
while i < legacy_color_code.chars().count() {
|
||||||
|
if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL {
|
||||||
|
let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap();
|
||||||
|
if let Ok(formatter) = ChatFormatting::from_code(formatting_code) {
|
||||||
|
if components.is_empty() || components.last().unwrap().text.is_empty() {
|
||||||
|
components.push(TextComponent::new("".to_string()));
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"applying formatter {:?} {:?}",
|
||||||
|
components.last_mut().unwrap().base.style,
|
||||||
|
formatter
|
||||||
|
);
|
||||||
|
components
|
||||||
|
.last_mut()
|
||||||
|
.unwrap()
|
||||||
|
.base
|
||||||
|
.style
|
||||||
|
.apply_formatting(formatter);
|
||||||
|
println!(
|
||||||
|
"applied formatter {:?}",
|
||||||
|
components.last_mut().unwrap().base.style
|
||||||
|
);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
if components.is_empty() {
|
||||||
|
components.push(TextComponent::new("".to_string()));
|
||||||
|
}
|
||||||
|
components
|
||||||
|
.last_mut()
|
||||||
|
.unwrap()
|
||||||
|
.text
|
||||||
|
.push(legacy_color_code.chars().nth(i).unwrap());
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the final component by using the first one as the base, and then adding the rest as siblings
|
||||||
|
let mut final_component = components.remove(0);
|
||||||
|
for component in components {
|
||||||
|
final_component.base.siblings.push(component.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
final_component.get()
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> TextComponent {
|
impl<'a> TextComponent {
|
||||||
pub fn new(text: String) -> Self {
|
pub fn new(text: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -17,4 +74,22 @@ impl<'a> TextComponent {
|
||||||
pub fn to_string(&self) -> String {
|
pub fn to_string(&self) -> String {
|
||||||
self.text.clone()
|
self.text.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get(self) -> Component {
|
||||||
|
Component::Text(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_color_code_to_component() {
|
||||||
|
let component = legacy_color_code_to_component("§lHello §r§1w§2o§3r§4l§5d");
|
||||||
|
assert_eq!(
|
||||||
|
component.to_ansi(),
|
||||||
|
"\u{1b}[38;2;170;0;170mHello world\u{1b}[m"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use minecraft_chat::{
|
||||||
component::Component,
|
component::Component,
|
||||||
style::{Ansi, ChatFormatting, TextColor},
|
style::{Ansi, ChatFormatting, TextColor},
|
||||||
};
|
};
|
||||||
use serde_json::{Value};
|
use serde_json::Value;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic_ansi_test() {
|
fn basic_ansi_test() {
|
||||||
|
@ -17,7 +17,7 @@ fn basic_ansi_test() {
|
||||||
let component = Component::new(&j).unwrap();
|
let component = Component::new(&j).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
component.to_ansi(),
|
component.to_ansi(),
|
||||||
"\x1b[1m\x1b[38;2;255;85;85mhello\x1b[m"
|
"\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue