aboutsummaryrefslogtreecommitdiff
path: root/azalea-chat/src/component.rs
diff options
context:
space:
mode:
authorKumpelinus <kumpelinus@jat.de>2025-05-01 20:26:04 +0200
committerGitHub <noreply@github.com>2025-05-01 13:26:04 -0500
commit11a74f215e28d7c3971c9894351567edb68ef0f8 (patch)
tree54f6be9b4152d71d0bf5016cc804854ccc6bcfb1 /azalea-chat/src/component.rs
parent4a7d21425ca644de776bd380abbfabc816710f43 (diff)
downloadazalea-drasl-11a74f215e28d7c3971c9894351567edb68ef0f8.tar.xz
Implement a to_html method for FormattedText (#208)
* Implement a to_html method for FormattedText Also fix a small issue with ansi formatting where it duplicated text. * cargo fmt * Make format conversion generic * cargo lint and fmt * Fix ascii conversion cleanup * Implement suggested changes * format, improve sanitization, add xss test --------- Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea-chat/src/component.rs')
-rw-r--r--azalea-chat/src/component.rs152
1 files changed, 120 insertions, 32 deletions
diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs
index e96ead43..ed7a0648 100644
--- a/azalea-chat/src/component.rs
+++ b/azalea-chat/src/component.rs
@@ -67,18 +67,24 @@ impl FormattedText {
}
}
- /// Convert this component into an
- /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code), so you
- /// can print it to your terminal and get styling.
+ /// Render all components into a single `String`, using your custom
+ /// closures to drive styling, text transformation, and final cleanup.
///
- /// This is technically a shortcut for
- /// [`FormattedText::to_ansi_with_custom_style`] with a default [`Style`]
- /// colored white.
+ /// # Type params
+ /// - `F`: `(running, component, default) -> (prefix, suffix)` for
+ /// per-component styling
+ /// - `S`: `&str -> String` for text tweaks (escaping, mapping, etc.)
+ /// - `C`: `&final_running_style -> String` for any trailing cleanup
///
- /// # Examples
+ /// # Args
+ /// - `style_formatter`: how to open/close each component’s style
+ /// - `text_formatter`: how to turn raw text into output text
+ /// - `cleanup_formatter`: emit after all components (e.g. reset codes)
+ /// - `default_style`: where to reset when a component’s `reset` is true
///
+ /// # Example
/// ```rust
- /// use azalea_chat::FormattedText;
+ /// use azalea_chat::{FormattedText, DEFAULT_STYLE};
/// use serde::de::Deserialize;
///
/// let component = FormattedText::deserialize(&serde_json::json!({
@@ -86,44 +92,126 @@ impl FormattedText {
/// "color": "red",
/// })).unwrap();
///
- /// println!("{}", component.to_ansi());
+ /// let ansi = component.to_custom_format(
+ /// |running, new, default| (running.compare_ansi(new, default), String::new()),
+ /// |text| text.to_string(),
+ /// |style| {
+ /// if !style.is_empty() {
+ /// "\u{1b}[m".to_string()
+ /// } else {
+ /// String::new()
+ /// }
+ /// },
+ /// &DEFAULT_STYLE,
+ /// );
+ /// println!("{}", ansi);
/// ```
- pub fn to_ansi(&self) -> String {
- // default the default_style to white if it's not set
- self.to_ansi_with_custom_style(&DEFAULT_STYLE)
- }
-
- /// Convert this component into an
- /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
- ///
- /// This is the same as [`FormattedText::to_ansi`], but you can specify a
- /// default [`Style`] to use.
- pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> 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
+ pub fn to_custom_format<F, S, C>(
+ &self,
+ mut style_formatter: F,
+ mut text_formatter: S,
+ mut cleanup_formatter: C,
+ default_style: &Style,
+ ) -> String
+ where
+ F: FnMut(&Style, &Style, &Style) -> (String, String),
+ S: FnMut(&str) -> String,
+ C: FnMut(&Style) -> String,
+ {
+ let mut output = String::new();
let mut running_style = Style::default();
for component in self.clone().into_iter() {
let component_text = match &component {
Self::Text(c) => c.text.to_string(),
- Self::Translatable(c) => c.to_string(),
+ Self::Translatable(c) => match c.read() {
+ Ok(c) => c.to_string(),
+ Err(_) => c.key.to_string(),
+ },
};
let component_style = &component.get_base().style;
- let ansi_text = running_style.compare_ansi(component_style, default_style);
- built_string.push_str(&ansi_text);
- built_string.push_str(&component_text);
+ let formatted_style = style_formatter(&running_style, component_style, default_style);
+ let formatted_text = text_formatter(&component_text);
- running_style.apply(component_style);
- }
+ output.push_str(&formatted_style.0);
+ output.push_str(&formatted_text);
+ output.push_str(&formatted_style.1);
- if !running_style.is_empty() {
- built_string.push_str("\u{1b}[m");
+ // Reset running style if required
+ if component_style.reset {
+ running_style = default_style.clone();
+ } else {
+ running_style.apply(component_style);
+ }
}
- built_string
+ output.push_str(&cleanup_formatter(&running_style));
+
+ output
+ }
+
+ /// Convert this component into an
+ /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
+ ///
+ /// This is the same as [`FormattedText::to_ansi`], but you can specify a
+ /// default [`Style`] to use.
+ pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
+ self.to_custom_format(
+ |running, new, default| (running.compare_ansi(new, default), "".to_owned()),
+ |text| text.to_string(),
+ |style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(),
+ default_style,
+ )
+ }
+
+ /// Convert this component into an
+ /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code), so you
+ /// can print it to your terminal and get styling.
+ ///
+ /// This is technically a shortcut for
+ /// [`FormattedText::to_ansi_with_custom_style`] with a default [`Style`]
+ /// colored white.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use azalea_chat::FormattedText;
+ /// use serde::de::Deserialize;
+ ///
+ /// let component = FormattedText::deserialize(&serde_json::json!({
+ /// "text": "Hello, world!",
+ /// "color": "red",
+ /// })).unwrap();
+ ///
+ /// println!("{}", component.to_ansi());
+ /// ```
+ pub fn to_ansi(&self) -> String {
+ self.to_ansi_with_custom_style(&DEFAULT_STYLE)
+ }
+
+ pub fn to_html(&self) -> String {
+ self.to_custom_format(
+ |running, new, _| {
+ (
+ format!(
+ "<span style=\"{}\">",
+ running.merged_with(new).get_html_style()
+ ),
+ "</span>".to_owned(),
+ )
+ },
+ |text| {
+ text.replace("&", "&amp;")
+ .replace("<", "&lt;")
+ // usually unnecessary but good for compatibility
+ .replace(">", "&gt;")
+ .replace("\n", "<br>")
+ },
+ |_| "".to_string(),
+ &DEFAULT_STYLE,
+ )
}
}