Skip to main content

rusty_rich/
json.rs

1//! JSON pretty printing — equivalent to Rich's `json.py`.
2
3use crate::console::{ConsoleOptions, RenderResult, Renderable};
4use crate::segment::Segment;
5use crate::style::Style;
6use crate::theme::Theme;
7use serde_json::Value;
8
9/// Render a JSON value with syntax highlighting.
10pub fn render_json(value: &Value) -> JsonRender {
11    JsonRender {
12        value: value.clone(),
13        indent: 2,
14        theme: None,
15        highlight: true,
16    }
17}
18
19/// Renders a JSON value with syntax highlighting via [`Renderable`].
20#[derive(Debug, Clone)]
21pub struct JsonRender {
22    value: Value,
23    indent: usize,
24    theme: Option<Theme>,
25    highlight: bool,
26}
27
28impl JsonRender {
29    /// Set the indentation width (number of spaces per level, default 2).
30    pub fn indent(mut self, n: usize) -> Self {
31        self.indent = n;
32        self
33    }
34    /// Set a custom theme for syntax highlighting colours.
35    pub fn theme(mut self, t: Theme) -> Self {
36        self.theme = Some(t);
37        self
38    }
39
40    fn style_for(&self, name: &str) -> Style {
41        if let Some(ref t) = self.theme {
42            t.get(name).cloned().unwrap_or(Style::new())
43        } else {
44            let theme = crate::theme::default_theme();
45            theme.get(name).cloned().unwrap_or(Style::new())
46        }
47    }
48}
49
50impl Renderable for JsonRender {
51    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
52        let formatted = format_json_value(&self.value, self.indent, 0, self.highlight, &|name| {
53            self.style_for(name)
54        });
55        let lines: Vec<Vec<Segment>> = formatted
56            .lines()
57            .map(|line| vec![Segment::new(line)])
58            .collect();
59        RenderResult {
60            lines,
61            items: Vec::new(),
62        }
63    }
64}
65
66fn format_json_value(
67    value: &Value,
68    indent: usize,
69    level: usize,
70    highlight: bool,
71    get_style: &dyn Fn(&str) -> Style,
72) -> String {
73    if !highlight {
74        return serde_json::to_string_pretty(value).unwrap_or_else(|_| format!("{value:?}"));
75    }
76
77    match value {
78        Value::Null => {
79            let s = get_style("json.null");
80            format!("{}{}null{}", s.to_ansi(), s.reset_ansi(), "")
81        }
82        Value::Bool(b) => {
83            let s = get_style("json.bool");
84            format!("{}{b}{}", s.to_ansi(), s.reset_ansi())
85        }
86        Value::Number(n) => {
87            let s = get_style("json.number");
88            format!("{}{n}{}", s.to_ansi(), s.reset_ansi())
89        }
90        Value::String(s) => {
91            let st = get_style("json.str");
92            format!(
93                "{}\"{}\"{}",
94                st.to_ansi(),
95                s.escape_default(),
96                st.reset_ansi()
97            )
98        }
99        Value::Array(arr) => {
100            if arr.is_empty() {
101                return "[]".to_string();
102            }
103            let brace = get_style("json.brace");
104            let mut out = format!("{}[{}", brace.to_ansi(), brace.reset_ansi());
105            let pad = " ".repeat(indent * (level + 1));
106            let close_pad = " ".repeat(indent * level);
107
108            for (i, item) in arr.iter().enumerate() {
109                let item_str = format_json_value(item, indent, level + 1, highlight, get_style);
110                out.push('\n');
111                out.push_str(&pad);
112                out.push_str(&item_str);
113                if i < arr.len() - 1 {
114                    out.push(',');
115                }
116            }
117            out.push('\n');
118            out.push_str(&close_pad);
119            out.push_str(&format!("{}]{}", brace.to_ansi(), brace.reset_ansi()));
120            out
121        }
122        Value::Object(obj) => {
123            if obj.is_empty() {
124                return "{}".to_string();
125            }
126            let brace = get_style("json.brace");
127            let key_style = get_style("json.key");
128            let mut out = format!("{}{{{}", brace.to_ansi(), brace.reset_ansi());
129            let pad = " ".repeat(indent * (level + 1));
130            let close_pad = " ".repeat(indent * level);
131            let keys: Vec<&String> = obj.keys().collect();
132
133            for (i, key) in keys.iter().enumerate() {
134                let val = &obj[*key];
135                let val_str = format_json_value(val, indent, level + 1, highlight, get_style);
136                out.push('\n');
137                out.push_str(&pad);
138                out.push_str(&format!(
139                    "{}\"{}\"{}: ",
140                    key_style.to_ansi(),
141                    key.escape_default(),
142                    key_style.reset_ansi()
143                ));
144                out.push_str(&val_str);
145                if i < keys.len() - 1 {
146                    out.push(',');
147                }
148            }
149            out.push('\n');
150            out.push_str(&close_pad);
151            out.push_str(&format!("{}}}{}", brace.to_ansi(), brace.reset_ansi()));
152            out
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_format_json() {
163        let v: Value = serde_json::from_str(r#"{"name": "Alice", "age": 30}"#).unwrap();
164        let rendered = render_json(&v);
165        let result = rendered.render(&ConsoleOptions::default());
166        assert!(result.to_ansi().contains("Alice"));
167    }
168}