diff --git a/Cargo.lock b/Cargo.lock index a52e124..73e0fc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,15 +87,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "colored" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" -dependencies = [ - "windows-sys", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -643,7 +634,6 @@ name = "text_components" version = "0.1.7" dependencies = [ "chrono", - "colored", "databake", "heck", "memchr", @@ -860,15 +850,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index e21d663..160b3be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,21 +33,20 @@ build = [ ] [dependencies] -colored = "3.1" rand = { version = "0.10", default-features = false, features = [ "std", "thread_rng", ] } serde = { version = "1.0", features = ["derive"], optional = true } simdnbt = { version = "0.10", optional = true } +uuid = { version = "1.23", features = ["v4", "serde"] } +supports-hyperlinks = "3.2.0" # Build dependencies heck = { version = "0.5", optional = true } proc-macro2 = { version = "1.0", optional = true } quote = { version = "1.0", optional = true } rustc-hash = { version = "2.1", optional = true } -serde_json = { version = "1.0", optional = true } -uuid = { version = "1.23", features = ["v4", "serde"] } -supports-hyperlinks = "3.2" +serde_json = { version = "1.0.149", optional = true } memchr = "2.8" smallvec = "1.15" ownable = { version = "1.0", optional = true } diff --git a/README.md b/README.md index cb96e79..1577617 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ component.resolve(resolutor).serialize(serializer); ### Displaying TextComponents -TextComponent implements Display for easy logging, as you can see, a component +TextComponent implements ToString for easy logging, as you can see, a component needs to be resolved before building it into any format, by default it uses a static reference to NoResolutor, but can be changed to a custom one with:\ (Resolutor must be static, or made inside the function call) @@ -55,12 +55,20 @@ reference to NoResolutor, but can be changed to a custom one with:\ set_display_resolutor(&Resolutor); ``` + A text component can be printed like a string like this: ```rs -println!("{}", component); -// With format (pretty): -println!("{:p}", component); +println!("{}", component.to_string()); +// With format (colorful): +println!("{}", component.log()); +``` + +If you want the log display format different to be able to parse it later, it can be done through `set_display_builder`, +which will need a function returning the built string, this is an example with the PrettyTextBuilder (the default one): + +```rs +set_display_builder(|component, resolutor| component.build(resolutor, PrettyTextBuilder)) ``` ### Roadmap diff --git a/examples/main.rs b/examples/main.rs index 8a1ac5f..e060816 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -166,6 +166,6 @@ fn main() { "\nNBT (SNBT):\ntellraw @a {}", component.build(&EmptyResolutor, NbtBuilder).to_snbt() ); - println!("\nText:\n{}", component); - println!("\nPretty Text:\n{:p}", component); + println!("\nText:\n{}", component.to_string()); + println!("\nPretty Text:\n{}", component.log()); } diff --git a/examples/nbt.rs b/examples/nbt.rs index 24d4077..0fe3fc3 100644 --- a/examples/nbt.rs +++ b/examples/nbt.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), String> { "tellraw @p {}", component.build(&NoResolutor, NbtBuilder).to_snbt() ); - println!("{:p}", component); + println!("{}", component.log()); let nbt = "Holly molly I can get TextComponents from NBTs!" .color(Color::Red) @@ -41,6 +41,6 @@ fn main() -> Result<(), String> { let component = RawTextComponent::from_nbt(&nbt) .ok_or(String::from("Cannot recompose the TextComponent!"))?; println!("{:?}", component); - println!("{:p}", component); + println!("{}", component.log()); Ok(()) } diff --git a/examples/serde.rs b/examples/serde.rs index 4c8427d..b35202b 100644 --- a/examples/serde.rs +++ b/examples/serde.rs @@ -13,5 +13,5 @@ fn main() { }", ) .unwrap(); - println!("{:p}", component) + println!("{}", component.log()) } diff --git a/examples/snbt.rs b/examples/snbt.rs index b021e6c..bcbee73 100644 --- a/examples/snbt.rs +++ b/examples/snbt.rs @@ -18,7 +18,7 @@ fn main() { match component { Ok(component) => { println!("{:?}", component); - println!("{:p}", component) + println!("{}", component.log()) } Err(e) => eprintln!("{}", e), } diff --git a/src/build.rs b/src/build.rs index 033bd7e..0269e89 100644 --- a/src/build.rs +++ b/src/build.rs @@ -69,6 +69,7 @@ pub fn build_translations(path: &str) -> TokenStream { let const_name = Ident::new(&const_name_str, Span::call_site()); stream.extend(quote! { + #[doc = #text] pub static #const_name: Translation<#param_count> = Translation(#key); }); } diff --git a/src/fmt.rs b/src/fmt.rs index a382a07..c966dcd 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,15 +1,15 @@ use crate::{ - RawTextComponent, + RawTextComponent, TextComponent, content::{Content, Object}, format::{Color, Format}, interactivity::{ClickEvent, Interactivity}, resolving::{BuildTarget, NoResolutor, TextResolutor}, }; -use colored::{ColoredString, Colorize}; use rand::random_range; use std::{ borrow::Cow, - fmt::{self, Debug, Display, Formatter, Pointer}, + fmt::{self, Debug, Formatter}, + sync::OnceLock, }; use supports_hyperlinks::supports_hyperlinks; @@ -62,22 +62,19 @@ const OBFUSCATION_CHARS: [char; 822] = [ pub struct TextBuilder; impl TextBuilder { - fn stringify_content<'a, R: TextResolutor<'a> + ?Sized, S: BuildTarget<'a>>( + fn stringify_content<'a, R: TextResolutor<'a> + ?Sized, S: BuildTarget<'a, Result = String>>( target: &S, resolutor: &R, component: &RawTextComponent<'a>, - ) -> S::Result - where - S::Result: From + ToString + Display, - { + ) -> String { match &component.content { - Content::Text { text } => text.to_string().into(), + Content::Text { text } => text.to_string(), Content::Translate(message) => { let translated = match resolutor.translate(&message.key) { Some(t) => t, None => match &message.fallback { - Some(f) => return f.to_string().into(), - None => return format!("[Translation: {}]", message.key).into(), + Some(f) => return f.to_string(), + None => return format!("[Translation: {}]", message.key), }, }; let parts = resolutor.split_translation(translated); @@ -88,11 +85,7 @@ impl TextBuilder { format: component.format.clone(), ..RawTextComponent::new() }; - built_parts.push( - target - .build_component(resolutor, &component_part) - .to_string(), - ); + built_parts.push(target.build_component(resolutor, &component_part)); if pos != 0 && let Some(args) = &message.args && pos <= args.len() @@ -104,25 +97,25 @@ impl TextBuilder { format: arg.format.mix(&component.format), interactions: arg.interactions.clone(), }; - built_parts.push(target.build_component(resolutor, &arg_part).to_string()); + built_parts.push(target.build_component(resolutor, &arg_part)); } } - built_parts.concat().into() + built_parts.concat() } - Content::Keybind { keybind } => format!("[Keybind: {}]", keybind).into(), - Content::Object(Object::Atlas { sprite, .. }) => format!("[Object: {}]", sprite).into(), + Content::Keybind { keybind } => format!("[Keybind: {}]", keybind), + Content::Object(Object::Atlas { sprite, .. }) => format!("[Object: {}]", sprite), Content::Object(Object::Player { player, .. }) => { if let Some(name) = &player.name { - return format!("[Head: {}]", name).into(); + return format!("[Head: {}]", name); } if let Some(id) = &player.id { - return format!("[Head: {:?}]", id).into(); + return format!("[Head: {:?}]", id); } - String::from("[Head]").into() + String::from("[Head]") } - Content::Resolvable(_) => String::from("[Resolvable]").into(), // Just in case ;) + Content::Resolvable(_) => String::from("[Resolvable]"), // Just in case ;) #[cfg(feature = "custom")] - Content::Custom { .. } => String::from("[Custom]").into(), + Content::Custom { .. } => String::from("[Custom]"), } } } @@ -143,14 +136,23 @@ impl<'a> BuildTarget<'a> for TextBuilder { } } +const URL_START: &str = "\x1b]8;;"; +const URL_SEPARATOR: &str = "\x1b\\"; +const URL_END: &str = "\x1b]8;;\x1b\\"; +const BOLD: &str = "\x1b[1m"; +const ITALIC: &str = "\x1b[3m"; +const UNDERLINED: &str = "\x1b[4m"; +const STRIKETHROUGH: &str = "\x1b[9m"; +const BG_COLOR: &str = "\x1b[48;2;"; +const RESET: &str = "\x1b[0m"; pub struct PrettyTextBuilder; impl<'a> BuildTarget<'a> for PrettyTextBuilder { - type Result = ColoredString; + type Result = String; fn build_component + ?Sized>( &self, resolutor: &R, component: &RawTextComponent<'a>, - ) -> ColoredString { + ) -> String { let mut final_text = TextBuilder::stringify_content(self, resolutor, component); if let Content::Translate(_) = component.content { @@ -167,16 +169,15 @@ impl<'a> BuildTarget<'a> for PrettyTextBuilder { format: child.format.mix(&component.format), interactions: child.interactions.clone(), }; - self.build_component(resolutor, &child).to_string() + self.build_component(resolutor, &child) }) .collect::>() .concat() - ) - .into(); + ); } if let Some(true) = component.format.obfuscated { - let obfuscated = final_text + final_text = final_text .chars() .map(|char| { if !char.is_whitespace() && !char.is_control() { @@ -185,35 +186,39 @@ impl<'a> BuildTarget<'a> for PrettyTextBuilder { char }) .collect::(); - final_text = ColoredString::from(obfuscated); } if let Some(color) = &component.format.color { - final_text = color.colorize_text(final_text.to_string()); + color.colorize_text(&mut final_text); } if let Some(true) = component.format.bold { - final_text = final_text.bold(); + final_text.insert_str(0, BOLD); } if let Some(true) = component.format.italic { - final_text = final_text.italic(); + final_text.insert_str(0, ITALIC); } if let Some(true) = component.format.underlined { - final_text = final_text.underline(); + final_text.insert_str(0, UNDERLINED); } if let Some(true) = component.format.strikethrough { - final_text = final_text.strikethrough(); + final_text.insert_str(0, STRIKETHROUGH); } if let Some(color) = component.format.shadow_color { - final_text = final_text.on_truecolor( - ((color >> 16) & 0xFF) as u8, - ((color >> 8) & 0xFF) as u8, - (color & 0xFF) as u8, + final_text.insert_str( + 0, + &format!( + "{BG_COLOR}{};{};{}m", + ((color >> 16) & 0xFF) as u8, + ((color >> 8) & 0xFF) as u8, + (color & 0xFF) as u8, + ), ); } if supports_hyperlinks() && let Some(ClickEvent::OpenUrl { url }) = &component.interactions.click { - final_text = format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", url, final_text).into(); + final_text = format!("{URL_START}{}{URL_SEPARATOR}{}{URL_END}", url, final_text); } + final_text.push_str(RESET); format!( "{}{}", @@ -228,46 +233,46 @@ impl<'a> BuildTarget<'a> for PrettyTextBuilder { format: child.format.mix(&component.format), interactions: child.interactions.clone(), }; - self.build_component(resolutor, &child).to_string() + self.build_component(resolutor, &child) }) .collect::>() .concat() ) - .into() } } impl<'a> RawTextComponent<'a> { - pub fn to_plain + ?Sized>(&self, resolutor: &R) -> String { + pub fn to_plain + ?Sized>(&self, resolutor: &'a R) -> String { self.build(resolutor, TextBuilder) } - pub fn to_pretty + ?Sized>(&self, resolutor: &R) -> ColoredString { + pub fn to_pretty + ?Sized>(&self, resolutor: &'a R) -> String { self.build(resolutor, PrettyTextBuilder) } } -static mut DISPLAY_RESOLUTOR: &'static dyn for<'a> TextResolutor<'a> = - &NoResolutor as &'static dyn for<'a> TextResolutor<'a>; -static mut INITIALIZED: bool = false; +static DISPLAY_RESOLUTOR: OnceLock<&'static (dyn TextResolutor<'static> + Sync)> = OnceLock::new(); +type DisplayBuilder = fn(&TextComponent, &'static (dyn TextResolutor<'static> + Sync)) -> String; +static DISPLAY_BUILDER: OnceLock = OnceLock::new(); -pub fn set_display_resolutor TextResolutor<'a>>(resolutor: &'static T) { - unsafe { - if !INITIALIZED { - DISPLAY_RESOLUTOR = resolutor as &'static dyn for<'a> TextResolutor<'a>; - INITIALIZED = true; - } - } +pub fn set_display_resolutor(resolutor: &'static (impl TextResolutor<'static> + Sync)) { + DISPLAY_RESOLUTOR.get_or_init(|| resolutor); +} +pub fn set_display_builder(f: DisplayBuilder) { + DISPLAY_BUILDER.get_or_init(|| f); } -impl Display for RawTextComponent<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", unsafe { self.to_plain(DISPLAY_RESOLUTOR) }) +#[allow(clippy::to_string_trait_impl)] +impl ToString for TextComponent { + fn to_string(&self) -> String { + self.to_plain(*DISPLAY_RESOLUTOR.get_or_init(|| &NoResolutor)) } } -impl<'a> Pointer for RawTextComponent<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", unsafe { self.to_pretty(DISPLAY_RESOLUTOR) }) +impl TextComponent { + pub fn log(&self) -> String { + let builder = DISPLAY_BUILDER + .get_or_init(|| |component, resolutor| component.build(resolutor, PrettyTextBuilder)); + builder(self, *DISPLAY_RESOLUTOR.get_or_init(|| &NoResolutor)) } } diff --git a/src/format.rs b/src/format.rs index c2d77cf..aa3978c 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,4 +1,3 @@ -use colored::{ColoredString, Colorize}; use std::{borrow::Cow, fmt::Display}; #[derive(Clone, PartialEq, Eq, Hash)] @@ -217,28 +216,34 @@ impl Color { } None } - - pub fn colorize_text(&self, text: impl Into) -> ColoredString { - let text = text.into(); - match self { - Color::Black => text.black(), - Color::DarkBlue => text.blue(), - Color::DarkGreen => text.green(), - Color::DarkAqua => text.cyan(), - Color::DarkRed => text.red(), - Color::DarkPurple => text.magenta(), - Color::Gold => text.yellow(), - Color::Gray => text.white(), - Color::DarkGray => text.bright_black(), - Color::Blue => text.bright_blue(), - Color::Green => text.bright_green(), - Color::Aqua => text.bright_cyan(), - Color::Red => text.bright_red(), - Color::LightPurple => text.bright_magenta(), - Color::Yellow => text.bright_yellow(), - Color::White => text.bright_white(), - Color::Rgb(r, g, b) => text.truecolor(*r, *g, *b), - } + pub fn colorize_text(&self, text: &mut String) { + let rgb = if let Color::Rgb(r, g, b) = self { + format!("\x1b[38;2;{r};{g};{b}m") + } else { + String::new() + }; + text.insert_str( + 0, + match self { + Color::Black => "\x1b[30m", + Color::DarkBlue => "\x1b[34m", + Color::DarkGreen => "\x1b[32m", + Color::DarkAqua => "\x1b[36m", + Color::DarkRed => "\x1b[31m", + Color::DarkPurple => "\x1b[35m", + Color::Gold => "\x1b[33m", + Color::Gray => "\x1b[37m", + Color::DarkGray => "\x1b[90m", + Color::Blue => "\x1b[94m", + Color::Green => "\x1b[92m", + Color::Aqua => "\x1b[96m", + Color::Red => "\x1b[91m", + Color::LightPurple => "\x1b[95m", + Color::Yellow => "\x1b[93m", + Color::White => "\x1b[97m", + Color::Rgb(..) => &rgb, + }, + ); } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index e75dfdc..f9d4191 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -57,7 +57,7 @@ impl Display for SnbtError { pub type SnbtResult = Result; impl<'a> RawTextComponent<'a> { - pub fn from_snbt(string: &'a str) -> SnbtResult> { + pub fn from_snbt(string: &str) -> SnbtResult> { parse_body(None, &mut string.chars().peekable()) } } diff --git a/src/resolving.rs b/src/resolving.rs index de26e1f..30aae57 100644 --- a/src/resolving.rs +++ b/src/resolving.rs @@ -159,6 +159,21 @@ impl<'a> RawTextComponent<'a> { ) -> S::Result { target.build_component(resolutor, &self.resolve(resolutor)) } + pub fn batch_build<'b, R: TextResolutor<'a> + ?Sized, S: BuildTarget<'a>>( + &self, + resolutors: Vec<&'b R>, + target: fn() -> S, + ) -> Vec<(&'b R, S::Result)> { + resolutors + .into_iter() + .map(|resolutor| { + ( + resolutor, + target().build_component(resolutor, &self.resolve(resolutor)), + ) + }) + .collect() + } /// Resolves all dynamic parts of the component recursively. /// @@ -209,6 +224,15 @@ impl<'a> RawTextComponent<'a> { component } + pub fn batch_resolve<'b, R: TextResolutor<'a> + ?Sized>( + &self, + resolutors: Vec<&'b R>, + ) -> Vec<(&'b R, RawTextComponent<'a>)> { + resolutors + .into_iter() + .map(|resolutor| (resolutor, self.resolve(resolutor))) + .collect() + } } /// A target format for building a resolved text component. @@ -230,5 +254,22 @@ pub trait BuildTarget<'a> { &self, resolutor: &R, component: &RawTextComponent<'a>, - ) -> Self::Result; + ) -> Self::Result + where + Self: Sized; +} + +impl<'a, T: BuildTarget<'a>> BuildTarget<'a> for Box { + type Result = T::Result; + + fn build_component + ?Sized>( + &self, + resolutor: &R, + component: &RawTextComponent<'a>, + ) -> Self::Result + where + Self: Sized, + { + (**self).build_component(resolutor, component) + } }