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 {
|
||||
Self {
|
||||
siblings: Vec::new(),
|
||||
style: Style::new(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
use serde_json;
|
||||
|
||||
use crate::{
|
||||
|
@ -11,8 +9,8 @@ use crate::{
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Component {
|
||||
TextComponent(TextComponent),
|
||||
TranslatableComponent(TranslatableComponent),
|
||||
Text(TextComponent),
|
||||
Translatable(TranslatableComponent),
|
||||
}
|
||||
|
||||
/// A chat component
|
||||
|
@ -23,7 +21,7 @@ impl Component {
|
|||
|
||||
// if it's primitive, make it a text component
|
||||
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(),
|
||||
)));
|
||||
}
|
||||
|
@ -31,7 +29,7 @@ impl Component {
|
|||
else if json.is_object() {
|
||||
if json.get("text").is_some() {
|
||||
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() {
|
||||
let translate = json.get("translate").unwrap().to_string();
|
||||
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
|
||||
// otherwise add the component to the array
|
||||
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()
|
||||
&& text_component.base.style.is_empty()
|
||||
{
|
||||
|
@ -51,15 +49,12 @@ impl Component {
|
|||
}
|
||||
with_array.push(StringOrComponent::Component(Component::new(&with[i])?));
|
||||
}
|
||||
component = Component::TranslatableComponent(TranslatableComponent::new(
|
||||
translate, with_array,
|
||||
));
|
||||
component =
|
||||
Component::Translatable(TranslatableComponent::new(translate, with_array));
|
||||
} else {
|
||||
// if it doesn't have a "with", just have the with_array be empty
|
||||
component = Component::TranslatableComponent(TranslatableComponent::new(
|
||||
translate,
|
||||
Vec::new(),
|
||||
));
|
||||
component =
|
||||
Component::Translatable(TranslatableComponent::new(translate, Vec::new()));
|
||||
}
|
||||
} else if json.get("score").is_some() {
|
||||
// object = GsonHelper.getAsJsonObject(jsonObject, "score");
|
||||
|
@ -157,15 +152,15 @@ impl Component {
|
|||
|
||||
pub fn get_base_mut(&mut self) -> &mut BaseComponent {
|
||||
match self {
|
||||
Self::TextComponent(c) => &mut c.base,
|
||||
Self::TranslatableComponent(c) => &mut c.base,
|
||||
Self::Text(c) => &mut c.base,
|
||||
Self::Translatable(c) => &mut c.base,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_base(&self) -> &BaseComponent {
|
||||
match self {
|
||||
Self::TextComponent(c) => &c.base,
|
||||
Self::TranslatableComponent(c) => &c.base,
|
||||
Self::Text(c) => &c.base,
|
||||
Self::Translatable(c) => &c.base,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,30 +177,17 @@ impl Component {
|
|||
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
|
||||
pub fn to_ansi(&self) -> String {
|
||||
// this contains the final string will all the ansi escape codes
|
||||
let mut built_string = String::new();
|
||||
// this style will update as we visit components
|
||||
let mut running_style = Style::new();
|
||||
let mut running_style = Style::default();
|
||||
|
||||
self.visit(&mut |component| {
|
||||
let component_text = match component {
|
||||
Self::TextComponent(c) => &c.text,
|
||||
Self::TranslatableComponent(c) => &c.key,
|
||||
for component in self.clone().into_iter() {
|
||||
let component_text = match &component {
|
||||
Self::Text(c) => &c.text,
|
||||
Self::Translatable(c) => &c.key,
|
||||
};
|
||||
let component_style = &component.get_base().style;
|
||||
|
||||
|
@ -214,12 +196,30 @@ impl Component {
|
|||
built_string.push_str(component_text);
|
||||
|
||||
running_style.apply(component_style);
|
||||
});
|
||||
}
|
||||
|
||||
if !running_style.is_empty() {
|
||||
built_string.push_str("\x1b[m");
|
||||
built_string.push_str("\u{1b}[m");
|
||||
}
|
||||
|
||||
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 {}
|
||||
impl Ansi {
|
||||
pub const BOLD: &'static str = "\x1b[1m";
|
||||
pub const ITALIC: &'static str = "\x1b[3m";
|
||||
pub const UNDERLINED: &'static str = "\x1b[4m";
|
||||
pub const STRIKETHROUGH: &'static str = "\x1b[9m";
|
||||
pub const OBFUSCATED: &'static str = "\x1b[8m";
|
||||
pub const RESET: &'static str = "\x1b[m";
|
||||
pub const BOLD: &'static str = "\u{1b}[1m";
|
||||
pub const ITALIC: &'static str = "\u{1b}[3m";
|
||||
pub const UNDERLINED: &'static str = "\u{1b}[4m";
|
||||
pub const STRIKETHROUGH: &'static str = "\u{1b}[9m";
|
||||
pub const OBFUSCATED: &'static str = "\u{1b}[8m";
|
||||
pub const RESET: &'static str = "\u{1b}[m";
|
||||
|
||||
pub fn rgb(value: u32) -> String {
|
||||
format!(
|
||||
"\x1b[38;2;{};{};{}m",
|
||||
"\u{1b}[38;2;{};{};{}m",
|
||||
(value >> 16) & 0xFF,
|
||||
(value >> 8) & 0xFF,
|
||||
value & 0xFF
|
||||
|
@ -159,6 +159,15 @@ impl<'a> ChatFormatting<'a> {
|
|||
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 {
|
||||
|
@ -212,7 +221,7 @@ pub struct Style {
|
|||
}
|
||||
|
||||
impl Style {
|
||||
pub fn new() -> Style {
|
||||
pub fn default() -> Style {
|
||||
Style {
|
||||
color: None,
|
||||
bold: None,
|
||||
|
@ -244,7 +253,7 @@ impl Style {
|
|||
obfuscated,
|
||||
}
|
||||
} else {
|
||||
Style::new()
|
||||
Style::default()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -275,7 +284,9 @@ impl Style {
|
|||
} else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) {
|
||||
true
|
||||
// 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();
|
||||
|
@ -287,7 +298,7 @@ impl Style {
|
|||
ansi_codes.push_str(Ansi::RESET);
|
||||
let mut updated_after = self.clone();
|
||||
updated_after.apply(after);
|
||||
(Style::new(), updated_after)
|
||||
(Style::default(), updated_after)
|
||||
} else {
|
||||
(self.clone(), after.clone())
|
||||
};
|
||||
|
@ -355,6 +366,37 @@ impl Style {
|
|||
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)]
|
||||
|
@ -422,4 +464,20 @@ mod tests {
|
|||
let ansi_difference = style_a.compare_ansi(&style_b);
|
||||
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)]
|
||||
pub struct TextComponent {
|
||||
|
@ -6,6 +6,63 @@ pub struct TextComponent {
|
|||
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 {
|
||||
pub fn new(text: String) -> Self {
|
||||
Self {
|
||||
|
@ -17,4 +74,22 @@ impl<'a> TextComponent {
|
|||
pub fn to_string(&self) -> String {
|
||||
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,
|
||||
style::{Ansi, ChatFormatting, TextColor},
|
||||
};
|
||||
use serde_json::{Value};
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
fn basic_ansi_test() {
|
||||
|
@ -17,7 +17,7 @@ fn basic_ansi_test() {
|
|||
let component = Component::new(&j).unwrap();
|
||||
assert_eq!(
|
||||
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