1
2
Fork 0
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:
mat 2021-12-11 11:38:12 -06:00
parent 5039f9668f
commit 6026c74430
5 changed files with 186 additions and 53 deletions

View file

@ -11,7 +11,7 @@ impl BaseComponent {
pub fn new() -> Self {
Self {
siblings: Vec::new(),
style: Style::new(),
style: Style::default(),
}
}
}

View file

@ -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>;
}

View file

@ -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)));
}
}

View file

@ -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"
);
}
}

View file

@ -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"
);
}