1use crate::{Widget, draw_text_span};
19use ftui_core::geometry::Rect;
20use ftui_render::frame::Frame;
21use ftui_style::Style;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum JsonToken {
26 Key(String),
28 StringVal(String),
30 Number(String),
32 Literal(String),
34 Punctuation(String),
36 Whitespace(String),
38 Newline,
40 Error(String),
42}
43
44#[derive(Debug, Clone)]
46pub struct JsonView {
47 source: String,
48 indent: usize,
49 key_style: Style,
50 string_style: Style,
51 number_style: Style,
52 literal_style: Style,
53 punct_style: Style,
54 error_style: Style,
55}
56
57impl Default for JsonView {
58 fn default() -> Self {
59 Self::new("")
60 }
61}
62
63impl JsonView {
64 #[must_use]
66 pub fn new(source: impl Into<String>) -> Self {
67 Self {
68 source: source.into(),
69 indent: 2,
70 key_style: Style::new().bold(),
71 string_style: Style::default(),
72 number_style: Style::default(),
73 literal_style: Style::default(),
74 punct_style: Style::default(),
75 error_style: Style::default(),
76 }
77 }
78
79 #[must_use]
81 pub fn with_indent(mut self, indent: usize) -> Self {
82 self.indent = indent;
83 self
84 }
85
86 #[must_use]
88 pub fn with_key_style(mut self, style: Style) -> Self {
89 self.key_style = style;
90 self
91 }
92
93 #[must_use]
95 pub fn with_string_style(mut self, style: Style) -> Self {
96 self.string_style = style;
97 self
98 }
99
100 #[must_use]
102 pub fn with_number_style(mut self, style: Style) -> Self {
103 self.number_style = style;
104 self
105 }
106
107 #[must_use]
109 pub fn with_literal_style(mut self, style: Style) -> Self {
110 self.literal_style = style;
111 self
112 }
113
114 #[must_use]
116 pub fn with_punct_style(mut self, style: Style) -> Self {
117 self.punct_style = style;
118 self
119 }
120
121 #[must_use]
123 pub fn with_error_style(mut self, style: Style) -> Self {
124 self.error_style = style;
125 self
126 }
127
128 pub fn set_source(&mut self, source: impl Into<String>) {
130 self.source = source.into();
131 }
132
133 #[must_use]
135 pub fn source(&self) -> &str {
136 &self.source
137 }
138
139 #[must_use]
141 pub fn formatted_lines(&self) -> Vec<Vec<JsonToken>> {
142 let trimmed = self.source.trim();
143 if trimmed.is_empty() {
144 return vec![];
145 }
146
147 let mut lines: Vec<Vec<JsonToken>> = Vec::new();
148 let mut current_line: Vec<JsonToken> = Vec::new();
149 let mut depth: usize = 0;
150 let mut chars = trimmed.chars().peekable();
151
152 while let Some(&ch) = chars.peek() {
153 match ch {
154 '{' | '[' => {
155 chars.next();
156 current_line.push(JsonToken::Punctuation(ch.to_string()));
157 skip_ws(&mut chars);
159 let next = chars.peek().copied();
160 if next == Some('}') || next == Some(']') {
161 let closing = chars.next().unwrap();
163 current_line.push(JsonToken::Punctuation(closing.to_string()));
164 skip_ws(&mut chars);
166 if chars.peek() == Some(&',') {
167 chars.next();
168 current_line.push(JsonToken::Punctuation(",".to_string()));
169 }
170 } else {
171 depth += 1;
172 lines.push(current_line);
173 current_line = vec![JsonToken::Whitespace(make_indent(
174 depth.min(32),
175 self.indent,
176 ))];
177 }
178 }
179 '}' | ']' => {
180 chars.next();
181 depth = depth.saturating_sub(1);
182 lines.push(current_line);
183 current_line = vec![
184 JsonToken::Whitespace(make_indent(depth, self.indent)),
185 JsonToken::Punctuation(ch.to_string()),
186 ];
187 skip_ws(&mut chars);
189 if chars.peek() == Some(&',') {
190 chars.next();
191 current_line.push(JsonToken::Punctuation(",".to_string()));
192 }
193 }
194 '"' => {
195 let s = read_string(&mut chars);
196 skip_ws(&mut chars);
197 if chars.peek() == Some(&':') {
198 current_line.push(JsonToken::Key(s));
200 chars.next();
201 current_line.push(JsonToken::Punctuation(": ".to_string()));
202 skip_ws(&mut chars);
203 } else {
204 current_line.push(JsonToken::StringVal(s));
205 skip_ws(&mut chars);
207 if chars.peek() == Some(&',') {
208 chars.next();
209 current_line.push(JsonToken::Punctuation(",".to_string()));
210 lines.push(current_line);
211 current_line = vec![JsonToken::Whitespace(make_indent(
212 depth.min(32),
213 self.indent,
214 ))];
215 }
216 }
217 }
218 ',' => {
219 chars.next();
220 current_line.push(JsonToken::Punctuation(",".to_string()));
221 lines.push(current_line);
222 current_line = vec![JsonToken::Whitespace(make_indent(
223 depth.min(32),
224 self.indent,
225 ))];
226 }
227 ':' => {
228 chars.next();
229 current_line.push(JsonToken::Punctuation(": ".to_string()));
230 skip_ws(&mut chars);
231 }
232 ' ' | '\t' | '\r' | '\n' => {
233 chars.next();
234 }
235 _ => {
236 let literal = read_literal(&mut chars);
238 let tok = classify_literal(&literal);
239 current_line.push(tok);
240 skip_ws(&mut chars);
242 if chars.peek() == Some(&',') {
243 chars.next();
244 current_line.push(JsonToken::Punctuation(",".to_string()));
245 lines.push(current_line);
246 current_line = vec![JsonToken::Whitespace(make_indent(
247 depth.min(32),
248 self.indent,
249 ))];
250 }
251 }
252 }
253 }
254
255 if !current_line.is_empty() {
256 lines.push(current_line);
257 }
258
259 lines
260 }
261}
262
263fn make_indent(depth: usize, width: usize) -> String {
264 " ".repeat(depth * width)
265}
266
267fn skip_ws(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
268 while let Some(&ch) = chars.peek() {
269 if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' {
270 chars.next();
271 } else {
272 break;
273 }
274 }
275}
276
277fn read_string(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
278 let mut s = String::new();
279 s.push('"');
280 chars.next(); let mut escaped = false;
282 for ch in chars.by_ref() {
283 s.push(ch);
284 if escaped {
285 escaped = false;
286 } else if ch == '\\' {
287 escaped = true;
288 } else if ch == '"' {
289 break;
290 }
291 }
292 s
293}
294
295fn read_literal(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
296 let mut s = String::new();
297 while let Some(&ch) = chars.peek() {
298 if ch == ','
299 || ch == '}'
300 || ch == ']'
301 || ch == ':'
302 || ch == ' '
303 || ch == '\n'
304 || ch == '\r'
305 || ch == '\t'
306 {
307 break;
308 }
309 s.push(ch);
310 chars.next();
311 }
312 s
313}
314
315fn classify_literal(s: &str) -> JsonToken {
316 match s {
317 "true" | "false" | "null" => JsonToken::Literal(s.to_string()),
318 _ => {
319 if s.bytes().all(|b| {
321 b.is_ascii_digit() || b == b'.' || b == b'-' || b == b'+' || b == b'e' || b == b'E'
322 }) && !s.is_empty()
323 {
324 JsonToken::Number(s.to_string())
325 } else {
326 JsonToken::Error(s.to_string())
327 }
328 }
329 }
330}
331
332impl Widget for JsonView {
333 fn render(&self, area: Rect, frame: &mut Frame) {
334 if area.width == 0 || area.height == 0 {
335 return;
336 }
337
338 let deg = frame.buffer.degradation;
339 let lines = self.formatted_lines();
340 let max_x = area.right();
341
342 for (row_idx, tokens) in lines.iter().enumerate() {
343 if row_idx >= area.height as usize {
344 break;
345 }
346
347 let y = area.y.saturating_add(row_idx as u16);
348 let mut x = area.x;
349
350 for token in tokens {
351 let (text, style) = match token {
352 JsonToken::Key(s) => (s.as_str(), self.key_style),
353 JsonToken::StringVal(s) => (s.as_str(), self.string_style),
354 JsonToken::Number(s) => (s.as_str(), self.number_style),
355 JsonToken::Literal(s) => (s.as_str(), self.literal_style),
356 JsonToken::Punctuation(s) => (s.as_str(), self.punct_style),
357 JsonToken::Whitespace(s) => (s.as_str(), Style::default()),
358 JsonToken::Error(s) => (s.as_str(), self.error_style),
359 JsonToken::Newline => continue,
360 };
361
362 if deg.apply_styling() {
363 x = draw_text_span(frame, x, y, text, style, max_x);
364 } else {
365 x = draw_text_span(frame, x, y, text, Style::default(), max_x);
366 }
367 }
368 }
369 }
370
371 fn is_essential(&self) -> bool {
372 false
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use ftui_render::frame::Frame;
380 use ftui_render::grapheme_pool::GraphemePool;
381
382 #[test]
383 fn empty_source() {
384 let view = JsonView::new("");
385 assert!(view.formatted_lines().is_empty());
386 }
387
388 #[test]
389 fn simple_object() {
390 let view = JsonView::new(r#"{"a": 1}"#);
391 let lines = view.formatted_lines();
392 assert!(lines.len() >= 3); }
394
395 #[test]
396 fn nested_object() {
397 let view = JsonView::new(r#"{"a": {"b": 2}}"#);
398 let lines = view.formatted_lines();
399 assert!(lines.len() >= 3);
400 }
401
402 #[test]
403 fn array() {
404 let view = JsonView::new(r#"[1, 2, 3]"#);
405 let lines = view.formatted_lines();
406 assert!(lines.len() >= 3);
407 }
408
409 #[test]
410 fn empty_object() {
411 let view = JsonView::new(r#"{}"#);
412 let lines = view.formatted_lines();
413 assert!(!lines.is_empty());
414 }
416
417 #[test]
418 fn empty_array() {
419 let view = JsonView::new(r#"[]"#);
420 let lines = view.formatted_lines();
421 assert!(!lines.is_empty());
422 }
423
424 #[test]
425 fn string_values() {
426 let view = JsonView::new(r#"{"msg": "hello world"}"#);
427 let lines = view.formatted_lines();
428 let has_string = lines.iter().any(|line| {
430 line.iter()
431 .any(|t| matches!(t, JsonToken::StringVal(s) if s.contains("hello")))
432 });
433 assert!(has_string);
434 }
435
436 #[test]
437 fn boolean_and_null() {
438 let view = JsonView::new(r#"{"a": true, "b": false, "c": null}"#);
439 let lines = view.formatted_lines();
440 let has_literal = lines.iter().any(|line| {
441 line.iter()
442 .any(|t| matches!(t, JsonToken::Literal(s) if s == "true"))
443 });
444 assert!(has_literal);
445 }
446
447 #[test]
448 fn numbers() {
449 let view = JsonView::new(r#"{"x": 42, "y": -3.14}"#);
450 let lines = view.formatted_lines();
451 let has_number = lines.iter().any(|line| {
452 line.iter()
453 .any(|t| matches!(t, JsonToken::Number(s) if s == "42"))
454 });
455 assert!(has_number);
456 }
457
458 #[test]
459 fn escaped_string() {
460 let view = JsonView::new(r#"{"msg": "hello \"world\""}"#);
461 let lines = view.formatted_lines();
462 let has_escaped = lines.iter().any(|line| {
463 line.iter()
464 .any(|t| matches!(t, JsonToken::StringVal(s) if s.contains("\\\"")))
465 });
466 assert!(has_escaped);
467 }
468
469 #[test]
470 fn indent_width() {
471 let view = JsonView::new(r#"{"a": 1}"#).with_indent(4);
472 let lines = view.formatted_lines();
473 let has_4_indent = lines.iter().any(|line| {
474 line.iter()
475 .any(|t| matches!(t, JsonToken::Whitespace(s) if s == " "))
476 });
477 assert!(has_4_indent);
478 }
479
480 #[test]
481 fn render_basic() {
482 let view = JsonView::new(r#"{"key": "value"}"#);
483 let mut pool = GraphemePool::new();
484 let mut frame = Frame::new(40, 10, &mut pool);
485 let area = Rect::new(0, 0, 40, 10);
486 view.render(area, &mut frame);
487
488 let cell = frame.buffer.get(0, 0).unwrap();
490 assert_eq!(cell.content.as_char(), Some('{'));
491 }
492
493 #[test]
494 fn render_zero_area() {
495 let view = JsonView::new(r#"{"a": 1}"#);
496 let mut pool = GraphemePool::new();
497 let mut frame = Frame::new(40, 10, &mut pool);
498 view.render(Rect::new(0, 0, 0, 0), &mut frame); }
500
501 #[test]
502 fn render_truncated_height() {
503 let view = JsonView::new(r#"{"a": 1, "b": 2, "c": 3}"#);
504 let mut pool = GraphemePool::new();
505 let mut frame = Frame::new(40, 2, &mut pool);
506 let area = Rect::new(0, 0, 40, 2);
507 view.render(area, &mut frame); }
509
510 #[test]
511 fn is_not_essential() {
512 let view = JsonView::new("");
513 assert!(!view.is_essential());
514 }
515
516 #[test]
517 fn default_impl() {
518 let view = JsonView::default();
519 assert!(view.source().is_empty());
520 }
521
522 #[test]
523 fn set_source() {
524 let mut view = JsonView::new("");
525 view.set_source(r#"{"a": 1}"#);
526 assert!(!view.formatted_lines().is_empty());
527 }
528
529 #[test]
530 fn plain_literal() {
531 let view = JsonView::new("42");
532 let lines = view.formatted_lines();
533 assert_eq!(lines.len(), 1);
534 }
535
536 #[test]
537 fn classify_literal_types() {
538 assert_eq!(
539 classify_literal("true"),
540 JsonToken::Literal("true".to_string())
541 );
542 assert_eq!(
543 classify_literal("false"),
544 JsonToken::Literal("false".to_string())
545 );
546 assert_eq!(
547 classify_literal("null"),
548 JsonToken::Literal("null".to_string())
549 );
550 assert_eq!(classify_literal("42"), JsonToken::Number("42".to_string()));
551 assert_eq!(
552 classify_literal("-3.14"),
553 JsonToken::Number("-3.14".to_string())
554 );
555 assert!(matches!(classify_literal("invalid!"), JsonToken::Error(_)));
556 }
557}