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 = chart_type_json_str(spec.chart_type);
170
171 let title = spec
172 .title
173 .as_deref()
174 .map(|t| json_string(t))
175 .unwrap_or_else(|| "null".to_string());
176
177 let y_count = spec.channels_by_name("y").len();
178
179 let mut parts = vec![
180 "\"type\":\"chart\"".to_string(),
181 format!("\"chart_type\":{}", chart_type),
182 format!("\"title\":{}", title),
183 format!("\"channel_count\":{}", spec.channels.len()),
184 format!("\"series_count\":{}", y_count),
185 ];
186
187 if let Some(ref xl) = spec.x_label {
188 parts.push(format!("\"x_label\":{}", json_string(xl)));
189 }
190 if let Some(ref yl) = spec.y_label {
191 parts.push(format!("\"y_label\":{}", json_string(yl)));
192 }
193
194 format!("{{{}}}", parts.join(","))
195}
196
197fn chart_type_json_str(ct: shape_value::content::ChartType) -> &'static str {
198 use shape_value::content::ChartType;
199 match ct {
200 ChartType::Line => "\"line\"",
201 ChartType::Bar => "\"bar\"",
202 ChartType::Scatter => "\"scatter\"",
203 ChartType::Area => "\"area\"",
204 ChartType::Candlestick => "\"candlestick\"",
205 ChartType::Histogram => "\"histogram\"",
206 ChartType::BoxPlot => "\"boxplot\"",
207 ChartType::Heatmap => "\"heatmap\"",
208 ChartType::Bubble => "\"bubble\"",
209 }
210}
211
212fn json_string(s: &str) -> String {
213 let mut out = String::with_capacity(s.len() + 2);
214 out.push('"');
215 for ch in s.chars() {
216 match ch {
217 '"' => out.push_str("\\\""),
218 '\\' => out.push_str("\\\\"),
219 '\n' => out.push_str("\\n"),
220 '\r' => out.push_str("\\r"),
221 '\t' => out.push_str("\\t"),
222 c if c < '\x20' => {
223 let _ = write!(out, "\\u{:04x}", c as u32);
224 }
225 c => out.push(c),
226 }
227 }
228 out.push('"');
229 out
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use shape_value::content::{ContentTable, NamedColor};
236
237 fn renderer() -> JsonRenderer {
238 JsonRenderer
239 }
240
241 #[test]
242 fn test_plain_text_json() {
243 let node = ContentNode::plain("hello");
244 let output = renderer().render(&node);
245 assert!(output.contains("\"type\":\"text\""));
246 assert!(output.contains("\"text\":\"hello\""));
247 }
248
249 #[test]
250 fn test_styled_text_json() {
251 let node = ContentNode::plain("bold")
252 .with_bold()
253 .with_fg(Color::Named(NamedColor::Red));
254 let output = renderer().render(&node);
255 assert!(output.contains("\"bold\":true"));
256 assert!(output.contains("\"fg\":\"red\""));
257 }
258
259 #[test]
260 fn test_rgb_color_json() {
261 let node = ContentNode::plain("rgb").with_fg(Color::Rgb(255, 0, 128));
262 let output = renderer().render(&node);
263 assert!(output.contains("\"r\":255"));
264 assert!(output.contains("\"g\":0"));
265 assert!(output.contains("\"b\":128"));
266 }
267
268 #[test]
269 fn test_table_json() {
270 let table = ContentNode::Table(ContentTable {
271 headers: vec!["A".into()],
272 rows: vec![vec![ContentNode::plain("1")]],
273 border: BorderStyle::Rounded,
274 max_rows: None,
275 column_types: None,
276 total_rows: None,
277 sortable: false,
278 });
279 let output = renderer().render(&table);
280 assert!(output.contains("\"type\":\"table\""));
281 assert!(output.contains("\"headers\":[\"A\"]"));
282 assert!(output.contains("\"border\":\"rounded\""));
283 assert!(output.contains("\"total_rows\":1"));
284 }
285
286 #[test]
287 fn test_code_json() {
288 let code = ContentNode::Code {
289 language: Some("rust".into()),
290 source: "fn main() {}".into(),
291 };
292 let output = renderer().render(&code);
293 assert!(output.contains("\"type\":\"code\""));
294 assert!(output.contains("\"language\":\"rust\""));
295 assert!(output.contains("\"source\":\"fn main() {}\""));
296 }
297
298 #[test]
299 fn test_code_no_language_json() {
300 let code = ContentNode::Code {
301 language: None,
302 source: "x".into(),
303 };
304 let output = renderer().render(&code);
305 assert!(output.contains("\"language\":null"));
306 }
307
308 #[test]
309 fn test_kv_json() {
310 let kv = ContentNode::KeyValue(vec![("k".into(), ContentNode::plain("v"))]);
311 let output = renderer().render(&kv);
312 assert!(output.contains("\"type\":\"kv\""));
313 assert!(output.contains("\"key\":\"k\""));
314 }
315
316 #[test]
317 fn test_fragment_json() {
318 let frag = ContentNode::Fragment(vec![ContentNode::plain("a"), ContentNode::plain("b")]);
319 let output = renderer().render(&frag);
320 assert!(output.contains("\"type\":\"fragment\""));
321 assert!(output.contains("\"children\":["));
322 }
323
324 #[test]
325 fn test_chart_json() {
326 let chart = ContentNode::Chart(shape_value::content::ChartSpec {
327 chart_type: shape_value::content::ChartType::Bar,
328 channels: vec![],
329 x_categories: None,
330 title: Some("Sales".into()),
331 x_label: None,
332 y_label: None,
333 width: None,
334 height: None,
335 echarts_options: None,
336 interactive: true,
337 });
338 let output = renderer().render(&chart);
339 assert!(output.contains("\"chart_type\":\"bar\""));
340 assert!(output.contains("\"title\":\"Sales\""));
341 }
342
343 #[test]
344 fn test_json_string_escaping() {
345 let node = ContentNode::plain("he said \"hello\" \\ \n\t");
346 let output = renderer().render(&node);
347 assert!(output.contains("\\\"hello\\\""));
348 assert!(output.contains("\\\\"));
349 assert!(output.contains("\\n"));
350 assert!(output.contains("\\t"));
351 }
352}