Skip to main content

ghast_protocol/
chat.rs

1//! Chat components.
2//!
3//! On the wire, chat is a JSON string; packets keep it as the raw string
4//! ([`ByteString`](crate::types::ByteString)). Parsing is opt-in: with the `chat-json`
5//! feature, [`parse`] and [`to_plain_text`] turn it into a JSON value or plain text.
6
7/// Removes legacy `§x` formatting codes from a string.
8pub fn strip_formatting(s: &str) -> String {
9    let mut out = String::with_capacity(s.len());
10    let mut it = s.chars();
11    while let Some(c) = it.next() {
12        if c == '\u{a7}' {
13            it.next();
14        } else {
15            out.push(c);
16        }
17    }
18    out
19}
20
21/// Parses a chat component JSON string.
22#[cfg(feature = "chat-json")]
23pub fn parse(json: &str) -> Result<serde_json::Value, serde_json::Error> {
24    serde_json::from_str(json)
25}
26
27/// Converts a chat component JSON string to plain text (`text`, `extra`, and
28/// `translate` keys with their `with` arguments appended). Invalid JSON is returned
29/// as-is, since some servers send bare strings.
30#[cfg(feature = "chat-json")]
31pub fn to_plain_text(json: &str) -> String {
32    fn walk(v: &serde_json::Value, out: &mut String) {
33        use serde_json::Value;
34        match v {
35            Value::String(s) => out.push_str(s),
36            Value::Array(a) => a.iter().for_each(|x| walk(x, out)),
37            Value::Object(o) => {
38                if let Some(t) = o.get("text") {
39                    walk(t, out);
40                }
41                if let Some(Value::String(t)) = o.get("translate") {
42                    out.push_str(t);
43                    if let Some(Value::Array(with)) = o.get("with") {
44                        for w in with {
45                            out.push(' ');
46                            walk(w, out);
47                        }
48                    }
49                }
50                if let Some(e) = o.get("extra") {
51                    walk(e, out);
52                }
53            }
54            Value::Number(n) => out.push_str(&n.to_string()),
55            Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
56            Value::Null => {}
57        }
58    }
59    match parse(json) {
60        Ok(v) => {
61            let mut out = String::new();
62            walk(&v, &mut out);
63            out
64        }
65        Err(_) => json.to_string(),
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn strip() {
75        assert_eq!(
76            strip_formatting("\u{a7}aHello \u{a7}lworld\u{a7}"),
77            "Hello world"
78        );
79    }
80
81    #[cfg(feature = "chat-json")]
82    #[test]
83    fn plain_text() {
84        assert_eq!(
85            to_plain_text(r#"{"text":"Hi ","extra":[{"text":"there","bold":true},"!"]}"#),
86            "Hi there!"
87        );
88        assert_eq!(to_plain_text(r#""bare""#), "bare");
89        assert_eq!(to_plain_text("not json"), "not json");
90    }
91}