1use crate::content_renderer::{ContentRenderer, RendererCapabilities};
7use shape_value::content::{
8 BorderStyle, ChartSpec, Color, ContentNode, ContentTable, NamedColor, Style,
9};
10use std::fmt::Write;
11
12pub struct JsonRenderer;
14
15impl ContentRenderer for JsonRenderer {
16 fn capabilities(&self) -> RendererCapabilities {
17 RendererCapabilities {
18 ansi: false,
19 unicode: true,
20 color: true,
21 interactive: false,
22 }
23 }
24
25 fn render(&self, content: &ContentNode) -> String {
26 render_node(content)
27 }
28}
29
30fn render_node(node: &ContentNode) -> String {
31 match node {
32 ContentNode::Text(st) => {
33 let spans: Vec<String> = st
34 .spans
35 .iter()
36 .map(|span| {
37 let style = render_style(&span.style);
38 format!(
39 "{{\"text\":{},\"style\":{}}}",
40 json_string(&span.text),
41 style
42 )
43 })
44 .collect();
45 format!("{{\"type\":\"text\",\"spans\":[{}]}}", spans.join(","))
46 }
47 ContentNode::Table(table) => render_table(table),
48 ContentNode::Code { language, source } => {
49 let lang = language
50 .as_deref()
51 .map(|l| json_string(l))
52 .unwrap_or_else(|| "null".to_string());
53 format!(
54 "{{\"type\":\"code\",\"language\":{},\"source\":{}}}",
55 lang,
56 json_string(source)
57 )
58 }
59 ContentNode::Chart(spec) => render_chart(spec),
60 ContentNode::KeyValue(pairs) => {
61 let entries: Vec<String> = pairs
62 .iter()
63 .map(|(k, v)| {
64 format!(
65 "{{\"key\":{},\"value\":{}}}",
66 json_string(k),
67 render_node(v)
68 )
69 })
70 .collect();
71 format!("{{\"type\":\"kv\",\"pairs\":[{}]}}", entries.join(","))
72 }
73 ContentNode::Fragment(parts) => {
74 let children: Vec<String> = parts.iter().map(render_node).collect();
75 format!(
76 "{{\"type\":\"fragment\",\"children\":[{}]}}",
77 children.join(",")
78 )
79 }
80 }
81}
82
83fn render_style(style: &Style) -> String {
84 let mut parts = Vec::new();
85 if style.bold {
86 parts.push("\"bold\":true".to_string());
87 }
88 if style.italic {
89 parts.push("\"italic\":true".to_string());
90 }
91 if style.underline {
92 parts.push("\"underline\":true".to_string());
93 }
94 if style.dim {
95 parts.push("\"dim\":true".to_string());
96 }
97 if let Some(ref color) = style.fg {
98 parts.push(format!("\"fg\":{}", render_color(color)));
99 }
100 if let Some(ref color) = style.bg {
101 parts.push(format!("\"bg\":{}", render_color(color)));
102 }
103 if parts.is_empty() {
104 "{}".to_string()
105 } else {
106 format!("{{{}}}", parts.join(","))
107 }
108}
109
110fn render_color(color: &Color) -> String {
111 match color {
112 Color::Named(named) => json_string(named_to_str(*named)),
113 Color::Rgb(r, g, b) => format!("{{\"r\":{},\"g\":{},\"b\":{}}}", r, g, b),
114 }
115}
116
117fn named_to_str(color: NamedColor) -> &'static str {
118 match color {
119 NamedColor::Red => "red",
120 NamedColor::Green => "green",
121 NamedColor::Blue => "blue",
122 NamedColor::Yellow => "yellow",
123 NamedColor::Magenta => "magenta",
124 NamedColor::Cyan => "cyan",
125 NamedColor::White => "white",
126 NamedColor::Default => "default",
127 }
128}
129
130fn render_table(table: &ContentTable) -> String {
131 let headers: Vec<String> = table.headers.iter().map(|h| json_string(h)).collect();
132
133 let limit = table.max_rows.unwrap_or(table.rows.len());
134 let display_rows = &table.rows[..limit.min(table.rows.len())];
135
136 let rows: Vec<String> = display_rows
137 .iter()
138 .map(|row| {
139 let cells: Vec<String> = row.iter().map(render_node).collect();
140 format!("[{}]", cells.join(","))
141 })
142 .collect();
143
144 let border = match table.border {
145 BorderStyle::Rounded => "\"rounded\"",
146 BorderStyle::Sharp => "\"sharp\"",
147 BorderStyle::Heavy => "\"heavy\"",
148 BorderStyle::Double => "\"double\"",
149 BorderStyle::Minimal => "\"minimal\"",
150 BorderStyle::None => "\"none\"",
151 };
152
153 let max_rows = table
154 .max_rows
155 .map(|n| n.to_string())
156 .unwrap_or_else(|| "null".to_string());
157
158 format!(
159 "{{\"type\":\"table\",\"headers\":[{}],\"rows\":[{}],\"border\":{},\"max_rows\":{},\"total_rows\":{}}}",
160 headers.join(","),
161 rows.join(","),
162 border,
163 max_rows,
164 table.rows.len()
165 )
166}
167
168fn render_chart(spec: &ChartSpec) -> String {
169 let chart_type = match spec.chart_type {
170 shape_value::content::ChartType::Line => "\"line\"",
171 shape_value::content::ChartType::Bar => "\"bar\"",
172 shape_value::content::ChartType::Scatter => "\"scatter\"",
173 shape_value::content::ChartType::Area => "\"area\"",
174 shape_value::content::ChartType::Candlestick => "\"candlestick\"",
175 shape_value::content::ChartType::Histogram => "\"histogram\"",
176 };
177
178 let title = spec
179 .title
180 .as_deref()
181 .map(|t| json_string(t))
182 .unwrap_or_else(|| "null".to_string());
183
184 let mut parts = vec![
185 format!("\"type\":\"chart\""),
186 format!("\"chart_type\":{}", chart_type),
187 format!("\"title\":{}", title),
188 format!("\"series_count\":{}", spec.series.len()),
189 ];
190
191 if let Some(ref xl) = spec.x_label {
192 parts.push(format!("\"x_label\":{}", json_string(xl)));
193 }
194 if let Some(ref yl) = spec.y_label {
195 parts.push(format!("\"y_label\":{}", json_string(yl)));
196 }
197
198 format!("{{{}}}", parts.join(","))
199}
200
201fn json_string(s: &str) -> String {
202 let mut out = String::with_capacity(s.len() + 2);
203 out.push('"');
204 for ch in s.chars() {
205 match ch {
206 '"' => out.push_str("\\\""),
207 '\\' => out.push_str("\\\\"),
208 '\n' => out.push_str("\\n"),
209 '\r' => out.push_str("\\r"),
210 '\t' => out.push_str("\\t"),
211 c if c < '\x20' => {
212 let _ = write!(out, "\\u{:04x}", c as u32);
213 }
214 c => out.push(c),
215 }
216 }
217 out.push('"');
218 out
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use shape_value::content::{ContentTable, NamedColor};
225
226 fn renderer() -> JsonRenderer {
227 JsonRenderer
228 }
229
230 #[test]
231 fn test_plain_text_json() {
232 let node = ContentNode::plain("hello");
233 let output = renderer().render(&node);
234 assert!(output.contains("\"type\":\"text\""));
235 assert!(output.contains("\"text\":\"hello\""));
236 }
237
238 #[test]
239 fn test_styled_text_json() {
240 let node = ContentNode::plain("bold")
241 .with_bold()
242 .with_fg(Color::Named(NamedColor::Red));
243 let output = renderer().render(&node);
244 assert!(output.contains("\"bold\":true"));
245 assert!(output.contains("\"fg\":\"red\""));
246 }
247
248 #[test]
249 fn test_rgb_color_json() {
250 let node = ContentNode::plain("rgb").with_fg(Color::Rgb(255, 0, 128));
251 let output = renderer().render(&node);
252 assert!(output.contains("\"r\":255"));
253 assert!(output.contains("\"g\":0"));
254 assert!(output.contains("\"b\":128"));
255 }
256
257 #[test]
258 fn test_table_json() {
259 let table = ContentNode::Table(ContentTable {
260 headers: vec!["A".into()],
261 rows: vec![vec![ContentNode::plain("1")]],
262 border: BorderStyle::Rounded,
263 max_rows: None,
264 column_types: None,
265 total_rows: None,
266 sortable: false,
267 });
268 let output = renderer().render(&table);
269 assert!(output.contains("\"type\":\"table\""));
270 assert!(output.contains("\"headers\":[\"A\"]"));
271 assert!(output.contains("\"border\":\"rounded\""));
272 assert!(output.contains("\"total_rows\":1"));
273 }
274
275 #[test]
276 fn test_code_json() {
277 let code = ContentNode::Code {
278 language: Some("rust".into()),
279 source: "fn main() {}".into(),
280 };
281 let output = renderer().render(&code);
282 assert!(output.contains("\"type\":\"code\""));
283 assert!(output.contains("\"language\":\"rust\""));
284 assert!(output.contains("\"source\":\"fn main() {}\""));
285 }
286
287 #[test]
288 fn test_code_no_language_json() {
289 let code = ContentNode::Code {
290 language: None,
291 source: "x".into(),
292 };
293 let output = renderer().render(&code);
294 assert!(output.contains("\"language\":null"));
295 }
296
297 #[test]
298 fn test_kv_json() {
299 let kv = ContentNode::KeyValue(vec![("k".into(), ContentNode::plain("v"))]);
300 let output = renderer().render(&kv);
301 assert!(output.contains("\"type\":\"kv\""));
302 assert!(output.contains("\"key\":\"k\""));
303 }
304
305 #[test]
306 fn test_fragment_json() {
307 let frag = ContentNode::Fragment(vec![ContentNode::plain("a"), ContentNode::plain("b")]);
308 let output = renderer().render(&frag);
309 assert!(output.contains("\"type\":\"fragment\""));
310 assert!(output.contains("\"children\":["));
311 }
312
313 #[test]
314 fn test_chart_json() {
315 let chart = ContentNode::Chart(shape_value::content::ChartSpec {
316 chart_type: shape_value::content::ChartType::Bar,
317 series: vec![],
318 title: Some("Sales".into()),
319 x_label: None,
320 y_label: None,
321 width: None,
322 height: None,
323 echarts_options: None,
324 interactive: true,
325 });
326 let output = renderer().render(&chart);
327 assert!(output.contains("\"chart_type\":\"bar\""));
328 assert!(output.contains("\"title\":\"Sales\""));
329 }
330
331 #[test]
332 fn test_json_string_escaping() {
333 let node = ContentNode::plain("he said \"hello\" \\ \n\t");
334 let output = renderer().render(&node);
335 assert!(output.contains("\\\"hello\\\""));
336 assert!(output.contains("\\\\"));
337 assert!(output.contains("\\n"));
338 assert!(output.contains("\\t"));
339 }
340}