Skip to main content

rich/
json.rs

1//! JSON pretty-printing.
2//!
3//! Port of upstream `rich/json.py`. [`Json`] parses a JSON string and renders it
4//! with 2-space indentation (matching Python's `json.dumps(indent=2)`) and the
5//! default JSON highlight colors.
6//!
7//! Non-ASCII strings render as UTF-8, matching upstream (`rich.json.JSON`
8//! defaults to `ensure_ascii=False`); object keys keep input order (serde_json's
9//! `preserve_order`). The one remaining caveat is **number formatting** for
10//! exotic values — exponent notation (`1e+20`, `1e-07`) and integers beyond
11//! i64/u64 can differ from CPython's `repr`. Custom indent/sort options are
12//! deferred — see docs/DIVERGENCES.md.
13
14use serde_json::Value;
15
16use crate::console::{Console, ConsoleOptions};
17use crate::errors::{Result, RichError};
18use crate::protocol::Renderable;
19use crate::segment::Segment;
20use crate::style::Style;
21
22/// A parsed JSON document, rendered with syntax highlighting. Mirrors `rich.json.JSON`.
23pub struct Json {
24    value: Value,
25    styles: JsonStyles,
26}
27
28struct JsonStyles {
29    brace: Style,
30    key: Style,
31    string: Style,
32    number: Style,
33    bool_true: Style,
34    bool_false: Style,
35    null: Style,
36}
37
38impl JsonStyles {
39    fn defaults() -> Self {
40        let s = |spec: &str| Style::parse(spec).expect("valid built-in style");
41        JsonStyles {
42            brace: s("bold"),
43            key: s("bold blue"),
44            string: s("green"),
45            number: s("bold cyan"),
46            bool_true: s("italic bright_green"),
47            bool_false: s("italic bright_red"),
48            null: s("italic magenta"),
49        }
50    }
51}
52
53impl Json {
54    /// Parse `text` as JSON. Returns an error if it is not valid JSON.
55    pub fn new(text: &str) -> Result<Self> {
56        let value = serde_json::from_str(text).map_err(|e| RichError::Json(e.to_string()))?;
57        Ok(Json {
58            value,
59            styles: JsonStyles::defaults(),
60        })
61    }
62
63    fn render_value(&self, value: &Value, level: usize, out: &mut Vec<Segment>) {
64        let brace = |text: &str| Segment::new(text.to_string(), Some(self.styles.brace.clone()));
65        let plain = |text: String| Segment::new(text, None);
66        match value {
67            Value::Null => out.push(Segment::new(
68                "null".to_string(),
69                Some(self.styles.null.clone()),
70            )),
71            Value::Bool(true) => out.push(Segment::new(
72                "true".to_string(),
73                Some(self.styles.bool_true.clone()),
74            )),
75            Value::Bool(false) => out.push(Segment::new(
76                "false".to_string(),
77                Some(self.styles.bool_false.clone()),
78            )),
79            Value::Number(number) => out.push(Segment::new(
80                number.to_string(),
81                Some(self.styles.number.clone()),
82            )),
83            Value::String(string) => out.push(Segment::new(
84                quote(string),
85                Some(self.styles.string.clone()),
86            )),
87            Value::Array(items) => {
88                out.push(brace("["));
89                if items.is_empty() {
90                    out.push(brace("]"));
91                    return;
92                }
93                out.push(plain("\n".to_string()));
94                let last = items.len() - 1;
95                for (index, item) in items.iter().enumerate() {
96                    out.push(plain("  ".repeat(level + 1)));
97                    self.render_value(item, level + 1, out);
98                    if index != last {
99                        out.push(plain(",".to_string()));
100                    }
101                    out.push(plain("\n".to_string()));
102                }
103                out.push(plain("  ".repeat(level)));
104                out.push(brace("]"));
105            }
106            Value::Object(map) => {
107                out.push(brace("{"));
108                if map.is_empty() {
109                    out.push(brace("}"));
110                    return;
111                }
112                out.push(plain("\n".to_string()));
113                let last = map.len() - 1;
114                for (index, (key, item)) in map.iter().enumerate() {
115                    out.push(plain("  ".repeat(level + 1)));
116                    out.push(Segment::new(quote(key), Some(self.styles.key.clone())));
117                    out.push(plain(": ".to_string()));
118                    self.render_value(item, level + 1, out);
119                    if index != last {
120                        out.push(plain(",".to_string()));
121                    }
122                    out.push(plain("\n".to_string()));
123                }
124                out.push(plain("  ".repeat(level)));
125                out.push(brace("}"));
126            }
127        }
128    }
129}
130
131impl Renderable for Json {
132    fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
133        let mut segments = Vec::new();
134        self.render_value(&self.value, 0, &mut segments);
135        segments
136    }
137}
138
139/// Serialize a string as a JSON string literal (quoted + escaped).
140fn quote(string: &str) -> String {
141    serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::color::ColorSystem;
148
149    fn render(text: &str) -> String {
150        let console = Console::builder()
151            .force_terminal(true)
152            .color_system(Some(ColorSystem::Truecolor))
153            .width(40)
154            .build();
155        console.render_to_string(&Json::new(text).unwrap())
156    }
157
158    #[test]
159    fn empty_collections_stay_inline() {
160        assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
161        assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
162    }
163
164    #[test]
165    fn object_with_scalars() {
166        assert_eq!(
167            render(r#"{"ok": false}"#),
168            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
169        );
170    }
171
172    #[test]
173    fn invalid_json_errors() {
174        assert!(Json::new("{not json}").is_err());
175    }
176
177    #[test]
178    fn non_ascii_stays_utf8_in_input_order() {
179        // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
180        // characters render as UTF-8 (not \uXXXX), and keys keep input order.
181        // (Byte-parity is guaranteed by the `json_unicode` golden.)
182        let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
183        assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
184        assert!(out.contains('\u{2764}'), "heart stays UTF-8");
185        let name_at = out.find("name").expect("name key present");
186        let emoji_at = out.find("emoji").expect("emoji key present");
187        assert!(name_at < emoji_at, "keys keep input order");
188    }
189}