1use std::collections::HashMap;
39
40use crate::console::{Console, ConsoleOptions};
41use crate::errors::{Result, RichError};
42use crate::measure::Measurement;
43use crate::protocol::Renderable;
44use crate::segment::Segment;
45use crate::style::Style;
46
47pub struct Json {
49 value: Node,
50 styles: JsonStyles,
51 no_wrap: bool,
53 #[cfg(feature = "json-escape-safe")]
54 escape_safe: bool,
55}
56
57#[derive(Debug)]
63enum Node {
64 Null,
65 Bool(bool),
66 Number(String),
67 NonFinite(&'static str),
71 Str(String),
72 Array(Vec<Node>),
73 Object(Vec<(String, Node)>),
74}
75
76impl Drop for Node {
77 fn drop(&mut self) {
84 let mut pending: Vec<Node> = Vec::new();
85 take_children(self, &mut pending);
86 while let Some(mut node) = pending.pop() {
87 take_children(&mut node, &mut pending);
88 }
91 }
92}
93
94fn take_children(node: &mut Node, out: &mut Vec<Node>) {
96 match node {
97 Node::Array(items) => out.append(items),
98 Node::Object(entries) => out.extend(entries.drain(..).map(|(_, value)| value)),
99 _ => {}
100 }
101}
102
103struct JsonStyles {
104 brace: Style,
105 key: Style,
106 string: Style,
107 number: Style,
108 bool_true: Style,
109 bool_false: Style,
110 null: Style,
111}
112
113impl JsonStyles {
114 fn defaults() -> Self {
115 let s = |spec: &str| Style::parse(spec).expect("valid built-in style");
116 JsonStyles {
117 brace: s("bold"),
118 key: s("bold blue"),
119 string: s("green"),
120 number: s("bold cyan"),
121 bool_true: s("italic bright_green"),
122 bool_false: s("italic bright_red"),
123 null: s("italic magenta"),
124 }
125 }
126}
127
128enum Task<'a> {
130 Value(&'a Node, usize),
132 Emit(Segment),
134}
135
136impl Json {
137 pub fn new(text: &str) -> Result<Self> {
139 Ok(Json {
140 value: Parser::new(text).parse_document()?,
141 styles: JsonStyles::defaults(),
142 no_wrap: false,
143 #[cfg(feature = "json-escape-safe")]
144 escape_safe: false,
145 })
146 }
147
148 #[cfg(feature = "json-escape-safe")]
153 pub fn escape_safe(mut self, enabled: bool) -> Self {
154 self.escape_safe = enabled;
155 self
156 }
157
158 #[must_use]
183 pub fn no_wrap(mut self, no_wrap: bool) -> Self {
184 self.no_wrap = no_wrap;
185 self
186 }
187
188 fn render_value(&self) -> Vec<Segment> {
193 let brace = |text: &str| Segment::new(text.to_string(), Some(self.styles.brace.clone()));
194 let plain = |text: String| Segment::new(text, None);
195
196 let mut out = Vec::new();
197 let mut stack = vec![Task::Value(&self.value, 0)];
198 while let Some(task) = stack.pop() {
199 let (node, level) = match task {
200 Task::Emit(segment) => {
201 out.push(segment);
202 continue;
203 }
204 Task::Value(node, level) => (node, level),
205 };
206 match node {
207 Node::Null => out.push(Segment::new(
208 "null".to_string(),
209 Some(self.styles.null.clone()),
210 )),
211 Node::Bool(true) => out.push(Segment::new(
212 "true".to_string(),
213 Some(self.styles.bool_true.clone()),
214 )),
215 Node::Bool(false) => out.push(Segment::new(
216 "false".to_string(),
217 Some(self.styles.bool_false.clone()),
218 )),
219 Node::Number(number) => out.push(Segment::new(
220 number.clone(),
221 Some(self.styles.number.clone()),
222 )),
223 Node::NonFinite(literal) => out.push(plain((*literal).to_string())),
224 Node::Str(string) => out.push(Segment::new(
225 quote(string),
226 Some(self.styles.string.clone()),
227 )),
228 Node::Array(items) => {
229 out.push(brace("["));
230 if items.is_empty() {
231 out.push(brace("]"));
232 continue;
233 }
234 out.push(plain("\n".to_string()));
235 stack.push(Task::Emit(brace("]")));
237 stack.push(Task::Emit(plain(" ".repeat(level))));
238 let last = items.len() - 1;
239 for (index, item) in items.iter().enumerate().rev() {
240 stack.push(Task::Emit(plain("\n".to_string())));
241 if index != last {
242 stack.push(Task::Emit(plain(",".to_string())));
243 }
244 stack.push(Task::Value(item, level + 1));
245 stack.push(Task::Emit(plain(" ".repeat(level + 1))));
246 }
247 }
248 Node::Object(entries) => {
249 out.push(brace("{"));
250 if entries.is_empty() {
251 out.push(brace("}"));
252 continue;
253 }
254 out.push(plain("\n".to_string()));
255 stack.push(Task::Emit(brace("}")));
256 stack.push(Task::Emit(plain(" ".repeat(level))));
257 let last = entries.len() - 1;
258 for (index, (key, item)) in entries.iter().enumerate().rev() {
259 stack.push(Task::Emit(plain("\n".to_string())));
260 if index != last {
261 stack.push(Task::Emit(plain(",".to_string())));
262 }
263 stack.push(Task::Value(item, level + 1));
264 stack.push(Task::Emit(plain(": ".to_string())));
265 stack.push(Task::Emit(Segment::new(
266 quote(key),
267 Some(self.styles.key.clone()),
268 )));
269 stack.push(Task::Emit(plain(" ".repeat(level + 1))));
270 }
271 }
272 }
273 }
274 out
275 }
276}
277
278impl Renderable for Json {
279 fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> Measurement {
282 let plain: String = self
283 .render_value()
284 .iter()
285 .map(|s| s.text.as_str())
286 .collect();
287 let (minimum, maximum) = crate::text::Text::new(plain).measurement();
288 Measurement::new(minimum, maximum)
289 }
290
291 fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
292 let segments = self.render_value();
293 #[cfg(feature = "json-escape-safe")]
294 if self.escape_safe {
295 return escape_safe_lines(&segments, options.max_width, self.no_wrap);
296 }
297 if self.no_wrap {
298 Segment::crop_lines(&segments, options.max_width)
302 } else {
303 Segment::fold_lines_words(&segments, options.max_width)
308 }
309 }
310}
311
312#[cfg(feature = "json-escape-safe")]
316fn escape_safe_lines(segments: &[Segment], width: usize, crop: bool) -> Vec<Segment> {
317 if width == 0 {
318 return Vec::new();
319 }
320 let lines = Segment::split_lines(segments);
321 let last = lines.len().saturating_sub(1);
322 let mut out = Vec::new();
323 for (line_index, line) in lines.into_iter().enumerate() {
324 let plain: String = line
325 .iter()
326 .filter(|s| !s.control)
327 .map(|s| s.text.as_str())
328 .collect();
329 if !plain.contains('\\') {
331 out.extend(if crop {
332 Segment::crop_lines(&line, width)
333 } else {
334 Segment::fold_lines_words(&line, width)
335 });
336 } else {
337 let (spans, _) = crate::cells::split_graphemes(&plain);
338 let mut atoms = Vec::new();
339 let mut index = 0;
340 while index < spans.len() {
341 let (start, mut end, mut cells) = spans[index];
342 if plain.as_bytes()[start] == b'\\' {
343 let escape_end = start
344 + if plain.as_bytes().get(start + 1) == Some(&b'u') {
345 6
346 } else {
347 2
348 };
349 while end < escape_end && index + 1 < spans.len() {
350 index += 1;
351 end = spans[index].1;
352 cells += spans[index].2;
353 }
354 if !crop && cells > width {
355 for offset in start..escape_end {
359 atoms.push((offset, offset + 1, 1));
360 }
361 if end > escape_end {
362 atoms.push((escape_end, end, 0));
363 }
364 index += 1;
365 continue;
366 }
367 }
368 atoms.push((start, end, cells));
369 index += 1;
370 }
371 let mut breaks = Vec::new();
372 let mut cells = 0;
373 let mut stop = plain.len();
374 for (start, _, atom_width) in atoms {
375 if cells + atom_width > width {
376 if crop {
377 stop = start;
378 break;
379 }
380 if cells > 0 {
381 breaks.push(start);
382 cells = 0;
383 }
384 }
385 cells += atom_width;
386 }
387 let mut position = 0;
388 let mut next = 0;
389 for segment in line {
390 if segment.control {
391 out.push(segment);
392 continue;
393 }
394 let mut buffer = String::new();
395 for ch in segment.text.chars() {
396 if position >= stop {
397 break;
398 }
399 if breaks.get(next) == Some(&position) {
400 if !buffer.is_empty() {
401 out.push(Segment::new(
402 std::mem::take(&mut buffer),
403 segment.style.clone(),
404 ));
405 }
406 out.push(Segment::line());
407 next += 1;
408 }
409 buffer.push(ch);
410 position += ch.len_utf8();
411 }
412 if !buffer.is_empty() {
413 out.push(Segment::new(buffer, segment.style));
414 }
415 }
416 }
417 if line_index != last {
418 out.push(Segment::line());
419 }
420 }
421 out
422}
423
424fn quote(string: &str) -> String {
426 serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
427}
428
429enum Frame {
431 Array(Vec<Node>),
432 Object {
433 entries: Vec<(String, Node)>,
434 seen: HashMap<String, usize>,
438 key: String,
440 },
441}
442
443struct Parser<'a> {
447 src: &'a str,
448 bytes: &'a [u8],
449 pos: usize,
450}
451
452impl<'a> Parser<'a> {
453 fn new(src: &'a str) -> Self {
454 Parser {
455 src,
456 bytes: src.as_bytes(),
457 pos: 0,
458 }
459 }
460
461 fn parse_document(&mut self) -> Result<Node> {
462 let value = self.parse_value()?;
463 self.skip_whitespace();
464 if self.pos != self.bytes.len() {
465 return Err(self.error("trailing characters"));
466 }
467 Ok(value)
468 }
469
470 fn parse_value(&mut self) -> Result<Node> {
472 let mut stack: Vec<Frame> = Vec::new();
473 let mut node: Node;
474
475 'descend: loop {
476 self.skip_whitespace();
477 match self.peek() {
478 Some(b'[') => {
479 self.pos += 1;
480 self.skip_whitespace();
481 if self.peek() == Some(b']') {
482 self.pos += 1;
483 node = Node::Array(Vec::new());
484 } else {
485 stack.push(Frame::Array(Vec::new()));
486 continue 'descend;
487 }
488 }
489 Some(b'{') => {
490 self.pos += 1;
491 self.skip_whitespace();
492 if self.peek() == Some(b'}') {
493 self.pos += 1;
494 node = Node::Object(Vec::new());
495 } else {
496 let key = self.parse_key()?;
497 stack.push(Frame::Object {
498 entries: Vec::new(),
499 seen: HashMap::new(),
500 key,
501 });
502 continue 'descend;
503 }
504 }
505 _ => node = self.parse_scalar()?,
506 }
507
508 loop {
511 let Some(frame) = stack.last_mut() else {
512 return Ok(node);
513 };
514 let closing = match frame {
515 Frame::Array(items) => {
516 items.push(node);
517 b']'
518 }
519 Frame::Object { entries, seen, key } => {
520 let key = std::mem::take(key);
521 match seen.get(&key) {
522 Some(&at) => entries[at].1 = node,
523 None => {
524 seen.insert(key.clone(), entries.len());
525 entries.push((key, node));
526 }
527 }
528 b'}'
529 }
530 };
531 self.skip_whitespace();
532 match self.peek() {
533 Some(b',') => {
534 self.pos += 1;
535 if closing == b'}' {
536 let next_key = self.parse_key()?;
537 if let Some(Frame::Object { key, .. }) = stack.last_mut() {
538 *key = next_key;
539 }
540 }
541 continue 'descend;
542 }
543 Some(byte) if byte == closing => {
544 self.pos += 1;
545 node = match stack.pop() {
546 Some(Frame::Array(items)) => Node::Array(items),
547 Some(Frame::Object { entries, .. }) => Node::Object(entries),
548 None => unreachable!("the frame was just borrowed"),
549 };
550 }
551 _ if closing == b']' => return Err(self.error("expected `,` or `]`")),
552 _ => return Err(self.error("expected `,` or `}`")),
553 }
554 }
555 }
556 }
557
558 fn parse_key(&mut self) -> Result<String> {
560 self.skip_whitespace();
561 if self.peek() != Some(b'"') {
562 return Err(self.error("key must be a string"));
563 }
564 let key = self.parse_string()?;
565 self.skip_whitespace();
566 if self.peek() != Some(b':') {
567 return Err(self.error("expected `:`"));
568 }
569 self.pos += 1;
570 Ok(key)
571 }
572
573 fn parse_scalar(&mut self) -> Result<Node> {
574 match self.peek() {
575 Some(b'"') => Ok(Node::Str(self.parse_string()?)),
576 Some(b't') => {
577 self.expect_literal("true")?;
578 Ok(Node::Bool(true))
579 }
580 Some(b'f') => {
581 self.expect_literal("false")?;
582 Ok(Node::Bool(false))
583 }
584 Some(b'n') => {
585 self.expect_literal("null")?;
586 Ok(Node::Null)
587 }
588 Some(b'N') => {
592 self.expect_literal("NaN")?;
593 Ok(Node::NonFinite("NaN"))
594 }
595 Some(b'I') => {
596 self.expect_literal("Infinity")?;
597 Ok(Node::NonFinite("Infinity"))
598 }
599 Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
600 self.pos += "-Infinity".len();
601 Ok(Node::NonFinite("-Infinity"))
602 }
603 Some(b'-' | b'0'..=b'9') => self.parse_number(),
604 Some(_) => Err(self.error("expected value")),
605 None => Err(self.error("EOF while parsing a value")),
606 }
607 }
608
609 fn parse_string(&mut self) -> Result<String> {
612 let start = self.pos;
613 let mut end = self.pos + 1;
614 loop {
615 match self.bytes.get(end) {
616 None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
617 Some(b'"') => {
618 end += 1;
619 break;
620 }
621 Some(b'\\') => {
622 end += 1;
623 match self.src[end..].chars().next() {
628 Some(ch) => end += ch.len_utf8(),
629 None => {
630 return Err(
631 self.error_at(self.bytes.len(), "EOF while parsing a string")
632 )
633 }
634 }
635 }
636 Some(_) => end += 1,
639 }
640 }
641 let decoded: String = serde_json::from_str(&self.src[start..end])
642 .map_err(|error| self.error_at(start, &describe(&error)))?;
643 self.pos = end;
644 Ok(decoded)
645 }
646
647 fn parse_number(&mut self) -> Result<Node> {
650 let start = self.pos;
651 let mut end = start;
652 while matches!(
653 self.bytes.get(end),
654 Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
655 ) {
656 end += 1;
657 }
658 let token = &self.src[start..end];
659 let digits = token.as_bytes();
660 let mut i = usize::from(digits.first() == Some(&b'-'));
661 if digits.get(i) == Some(&b'0') {
662 i += 1;
663 } else {
664 let first = i;
665 while digits.get(i).is_some_and(u8::is_ascii_digit) {
666 i += 1;
667 }
668 if i == first {
669 return Err(self.error_at(start, "invalid number"));
670 }
671 }
672 let mut floating = false;
673 if digits.get(i) == Some(&b'.') {
674 floating = true;
675 i += 1;
676 let first = i;
677 while digits.get(i).is_some_and(u8::is_ascii_digit) {
678 i += 1;
679 }
680 if i == first {
681 return Err(self.error_at(start, "invalid number"));
682 }
683 }
684 if matches!(digits.get(i), Some(b'e' | b'E')) {
685 floating = true;
686 i += 1;
687 if matches!(digits.get(i), Some(b'+' | b'-')) {
688 i += 1;
689 }
690 let first = i;
691 while digits.get(i).is_some_and(u8::is_ascii_digit) {
692 i += 1;
693 }
694 if i == first {
695 return Err(self.error_at(start, "invalid number"));
696 }
697 }
698 if i != digits.len() {
699 return Err(self.error_at(start, "invalid number"));
700 }
701 self.pos = end;
702 if !floating {
703 return Ok(Node::Number(
704 if token == "-0" { "0" } else { token }.to_string(),
705 ));
706 }
707 let value: f64 = token
708 .parse()
709 .map_err(|_| self.error_at(start, "invalid number"))?;
710 if value.is_infinite() {
711 return Ok(Node::NonFinite(if value.is_sign_negative() {
712 "-Infinity"
713 } else {
714 "Infinity"
715 }));
716 }
717 let number: serde_json::Number =
718 serde_json::from_str(token).map_err(|error| self.error_at(start, &describe(&error)))?;
719 Ok(Node::Number(number.to_string()))
720 }
721
722 fn expect_literal(&mut self, literal: &str) -> Result<()> {
723 if self.src[self.pos..].starts_with(literal) {
724 self.pos += literal.len();
725 Ok(())
726 } else {
727 Err(self.error("expected value"))
728 }
729 }
730
731 fn peek(&self) -> Option<u8> {
732 self.bytes.get(self.pos).copied()
733 }
734
735 fn skip_whitespace(&mut self) {
736 while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
737 self.pos += 1;
738 }
739 }
740
741 fn error(&self, message: &str) -> RichError {
742 self.error_at(self.pos, message)
743 }
744
745 fn error_at(&self, pos: usize, message: &str) -> RichError {
746 let (line, column) = self.line_column(pos);
747 RichError::Json(format!("{message} at line {line} column {column}"))
748 }
749
750 fn line_column(&self, pos: usize) -> (usize, usize) {
751 let mut pos = pos.min(self.src.len());
752 while !self.src.is_char_boundary(pos) {
753 pos -= 1;
754 }
755 let before = &self.src[..pos];
756 let line = 1 + before.matches('\n').count();
757 let column = before
758 .rsplit('\n')
759 .next()
760 .map_or(0, |tail| tail.chars().count())
761 + 1;
762 (line, column)
763 }
764}
765
766fn describe(error: &serde_json::Error) -> String {
769 let text = error.to_string();
770 match text.find(" at line ") {
771 Some(at) => text[..at].to_string(),
772 None => text,
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use crate::color::ColorSystem;
780
781 fn render(text: &str) -> String {
782 let console = Console::builder()
783 .force_terminal(true)
784 .color_system(Some(ColorSystem::Truecolor))
785 .width(40)
786 .build();
787 console.render_to_string(&Json::new(text).unwrap())
788 }
789
790 fn render_plain(text: &str, width: usize) -> String {
791 let console = Console::builder().width(width).no_color(true).build();
792 console.render_to_string(&Json::new(text).expect("valid json"))
793 }
794
795 #[test]
796 fn empty_collections_stay_inline() {
797 assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
798 assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
799 }
800
801 #[test]
802 fn object_with_scalars() {
803 assert_eq!(
804 render(r#"{"ok": false}"#),
805 "\x1b[1m{\x1b[0m\n \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
806 );
807 }
808
809 #[test]
810 fn invalid_json_errors() {
811 assert!(Json::new("{not json}").is_err());
812 }
813
814 #[test]
815 fn non_ascii_stays_utf8_in_input_order() {
816 let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
820 assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
821 assert!(out.contains('\u{2764}'), "heart stays UTF-8");
822 let name_at = out.find("name").expect("name key present");
823 let emoji_at = out.find("emoji").expect("emoji key present");
824 assert!(name_at < emoji_at, "keys keep input order");
825 }
826
827 #[test]
828 fn a_long_value_is_wrapped_rather_than_cropped() {
829 let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
832 let out = render_plain(&payload, 40);
833 assert_eq!(
834 out.matches('y').count(),
835 120,
836 "characters were dropped:
837{out}"
838 );
839 }
840
841 #[test]
850 fn wrapping_breaks_at_word_boundaries() {
851 let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
852 assert_eq!(
853 render_plain(payload, 40),
854 "{\n \"k\": \"the quick brown fox jumps over \n\
855 the lazy dog and keeps running for a \n\
856 very long time indeed\"\n}"
857 );
858 }
859
860 #[cfg(feature = "json-escape-safe")]
861 #[test]
862 fn adjusted_escape_boundaries_preserve_the_remaining_payload() {
863 let input = format!(r#"{{"v":"aa\u0001{}"}}"#, "b".repeat(40));
864 for width in 6..=20 {
865 let output = Console::builder()
866 .width(width)
867 .force_terminal(false)
868 .build()
869 .render_to_string(&Json::new(&input).unwrap().escape_safe(true));
870 assert_eq!(output.matches('b').count(), 40, "width {width}: {output:?}");
871 }
872 }
873
874 #[cfg(feature = "json-escape-safe")]
875 #[test]
876 fn wrapping_keeps_json_escapes_atomic_at_narrow_widths() {
877 let payload = r#"{"v":"a\"b\\c\nd\u0001e"}"#;
878 for width in 8..=14 {
879 let output = Console::builder()
880 .width(width)
881 .force_terminal(false)
882 .build()
883 .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
884 for line in output.lines() {
885 let bytes = line.as_bytes();
886 let mut index = 0;
887 while index < bytes.len() {
888 if bytes[index] != b'\\' {
889 index += 1;
890 continue;
891 }
892 assert!(
893 index + 1 < bytes.len(),
894 "split escape at width {width}: {output:?}"
895 );
896 if bytes[index + 1] == b'u' {
897 assert!(
898 index + 6 <= bytes.len(),
899 "split unicode escape at width {width}: {output:?}"
900 );
901 index += 6;
902 } else {
903 index += 2;
904 }
905 }
906 }
907 }
908 }
909
910 #[cfg(feature = "json-escape-safe")]
911 #[test]
912 fn escape_folding_preserves_bytes_even_below_the_escape_width() {
913 let payload = r#"{"v":"a\"b\\c\nd\u0001eeeeeeeeeeee"}"#;
914 let wide = Console::builder()
915 .width(100)
916 .force_terminal(false)
917 .build()
918 .render_to_string(&Json::new(payload).unwrap().escape_safe(true))
919 .replace('\n', "");
920 for width in 1..=20 {
921 let output = Console::builder()
922 .width(width)
923 .force_terminal(false)
924 .build()
925 .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
926 assert_eq!(output.replace('\n', ""), wide, "width {width}");
927 assert!(output
928 .lines()
929 .all(|line| crate::cells::cell_len(line) <= width));
930 }
931 }
932
933 #[cfg(feature = "json-escape-safe")]
934 #[test]
935 fn escape_cropping_never_emits_a_partial_escape() {
936 let payload = r#""a\"b\\c\nd\u0001eeee""#;
937 for width in 1..=24 {
938 let output = Console::builder()
939 .width(width)
940 .force_terminal(false)
941 .build()
942 .render_to_string(&Json::new(payload).unwrap().no_wrap(true).escape_safe(true));
943 let mut chars = output.chars();
944 while let Some(c) = chars.next() {
945 if c == '\\' {
946 let next = chars.next().expect("complete short escape");
947 if next == 'u' {
948 for _ in 0..4 {
949 assert!(chars.next().is_some_and(|c| c.is_ascii_hexdigit()));
950 }
951 }
952 }
953 }
954 }
955 }
956
957 #[test]
967 fn a_nested_document_is_cropped_rather_than_wrapped() {
968 let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
969 let console = Console::builder().width(40).no_color(true).build();
970 let json = Json::new(payload).expect("valid json").no_wrap(true);
971 assert_eq!(
972 console.render_to_string(&json),
973 "{\n \"k\": \"the quick brown fox jumps over t\n}"
974 );
975
976 assert_eq!(
979 render_plain(payload, 40),
980 "{\n \"k\": \"the quick brown fox jumps over \n\
981 the lazy dog and keeps running for a \n\
982 very long time indeed\"\n}"
983 );
984 }
985
986 #[test]
990 fn cropping_never_splits_a_wide_character() {
991 let console = Console::builder().width(12).no_color(true).build();
992 let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
993 .expect("valid json")
994 .no_wrap(true);
995 for line in console.render_to_string(&json).lines() {
996 assert!(
997 crate::cells::cell_len(line) <= 12,
998 "line {line:?} overflows the crop"
999 );
1000 }
1001 }
1002
1003 #[test]
1007 fn floats_round_trip_exactly() {
1008 for literal in [
1009 "-938371.9565467801",
1010 "0.1",
1011 "1.7976931348623157e308",
1012 "5e-324",
1013 "3.141592653589793",
1014 ] {
1015 let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
1016 let rendered: String = out
1017 .split(':')
1018 .nth(1)
1019 .expect("a value after the key")
1020 .trim()
1021 .trim_end_matches(['}', ' ', '\n'])
1022 .to_string();
1023 let want: f64 = literal.parse().expect("literal parses");
1024 let got: f64 = rendered
1025 .parse()
1026 .unwrap_or_else(|_| panic!("rendered {rendered:?}"));
1027 assert_eq!(
1028 got.to_bits(),
1029 want.to_bits(),
1030 "{literal} rendered as {rendered} — a different double"
1031 );
1032 }
1033 }
1034
1035 #[test]
1039 fn deep_nesting_is_not_rejected() {
1040 for depth in [128, 129, 200, 1000] {
1041 let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
1042 let json = Json::new(&payload)
1043 .unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
1044 let console = Console::builder()
1045 .width(4 * depth + 8)
1046 .no_color(true)
1047 .build();
1048 let out = console.render_to_string(&json);
1049 assert_eq!(
1050 out.matches('[').count(),
1051 depth,
1052 "depth {depth} did not render every level"
1053 );
1054 }
1055 }
1056
1057 #[test]
1061 fn very_deep_nesting_does_not_overflow_the_stack() {
1062 let depth = 100_000;
1063 let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
1064 let json = Json::new(&payload).expect("deep document parses");
1065 drop(json);
1066 }
1067
1068 #[test]
1076 fn non_finite_numbers_render_unstyled() {
1077 assert_eq!(
1078 render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
1079 "\x1b[1m{\x1b[0m\n \x1b[1;34m\"a\"\x1b[0m: NaN,\n \x1b[1;34m\"b\"\x1b[0m: \
1080 Infinity,\n \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n \x1b[1;34m\"d\"\x1b[0m: \
1081 \x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
1082 );
1083 for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
1085 assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
1086 }
1087 }
1088
1089 #[test]
1092 fn acceptance_matches_serde_json() {
1093 let samples = [
1094 "{}",
1095 "[]",
1096 " {\t\"a\" :\n1 } ",
1097 r#"{"a": 1, "a": 2}"#,
1098 r#"{"a": [1, {"b": null}], "c": "x"}"#,
1099 "0",
1100 "-0",
1101 "0.0",
1102 "1e10",
1103 "1E+10",
1104 "1e-7",
1105 "12345678901234567890",
1106 "-12345678901234567890123456789012345",
1107 "01",
1108 "1.",
1109 ".1",
1110 "+1",
1111 "1e",
1112 "-",
1113 "--1",
1114 "-i",
1115 "-Inf",
1116 "Infinit",
1117 "NAN",
1118 "1 2",
1119 "",
1120 " ",
1121 "{",
1122 "[",
1123 "]",
1124 "}",
1125 "[,]",
1126 "[1,]",
1127 r#"{"a": 1,}"#,
1128 r#"{a: 1}"#,
1129 r#"{'a': 1}"#,
1130 r#"{"a" 1}"#,
1131 "[1 2]",
1132 "truex",
1133 "tru",
1134 "nul",
1135 r#""unterminated"#,
1136 r#""\q""#,
1137 r#""é""#,
1138 r#""😀""#,
1139 r#""\ud800""#,
1140 "\"raw\nnewline\"",
1141 r#""café ❤""#,
1142 "\"\u{e9}\\\"",
1143 "\u{feff}{}",
1144 "[[[[1]]]]",
1145 ];
1146 for sample in samples {
1147 let ours = Json::new(sample).is_ok();
1148 let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
1149 assert_eq!(ours, theirs, "disagreed about {sample:?}");
1150 }
1151 }
1152
1153 #[test]
1154 fn python_numbers_preserve_large_integers_and_overflow() {
1155 for number in [
1156 "1234567890123456789012345678901234567890",
1157 "-1234567890123456789012345678901234567890",
1158 ] {
1159 assert_eq!(render_plain(number, 100), number);
1160 }
1161 assert_eq!(render_plain("-0", 100), "0");
1162 assert_eq!(render_plain("1e400", 100), "Infinity");
1163 assert_eq!(render_plain("-1e999", 100), "-Infinity");
1164 for invalid in ["01", "-01", "1.e2", "1e+", "1e400x", "--1", "1+2", ".1"] {
1165 assert!(Json::new(invalid).is_err(), "accepted {invalid}");
1166 }
1167 }
1168
1169 #[test]
1175 fn the_parsed_tree_matches_serde_json() {
1176 let samples = [
1177 r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
1178 r#"{"a": 1, "b": 2, "a": 3}"#,
1179 r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
1180 r#"{"k": "A\t\"x\"A\\\/é"}"#,
1181 r#"[0, -0.5, 1e10, 1E+10, 1e-7, 12345678901234567890, 1.7976931348623157e308]"#,
1182 r#"{"café": "❤", "": ""}"#,
1183 "[]",
1184 "{}",
1185 "\"top level\"",
1186 "1234",
1187 ];
1188 for sample in samples {
1189 let reference: serde_json::Value =
1190 serde_json::from_str(sample).expect("sample is valid JSON");
1191 assert_eq!(
1192 render_plain(sample, 10_000),
1193 serde_json::to_string_pretty(&reference).expect("value re-serialises"),
1194 "diverged on {sample}"
1195 );
1196 }
1197 }
1198
1199 #[test]
1202 fn a_repeated_key_keeps_its_position_and_last_value() {
1203 assert_eq!(
1204 render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
1205 "{\n \"a\": 3,\n \"b\": 2\n}"
1206 );
1207 }
1208
1209 #[test]
1212 fn escapes_are_re_encoded_like_dumps() {
1213 assert_eq!(
1214 render_plain(r#"{"k": "A\t\"x\""}"#, 60),
1215 "{\n \"k\": \"A\\t\\\"x\\\"\"\n}"
1216 );
1217 }
1218}