Skip to main content

rich/
json.rs

1//! JSON pretty-printing.
2//!
3//! Port of upstream `rich/json.py`. [`Json`] parses a JSON string and renders it
4//! with 2-space indentation (matching Python's `json.dumps(indent=2)`) and the
5//! default JSON highlight colors.
6//!
7//! Non-ASCII strings render as UTF-8, matching upstream (`rich.json.JSON`
8//! defaults to `ensure_ascii=False`); object keys keep input order, and a
9//! repeated key keeps its first position but its last value — what both
10//! `dict` and serde_json's `preserve_order` do. The one remaining caveat is
11//! **number formatting** for exotic values — exponent notation (`1e+20`,
12//! `1e-07`) and integers beyond i64/u64 can differ from CPython's `repr`.
13//! Custom indent/sort options are deferred — see docs/DIVERGENCES.md.
14//!
15//! ## Why the parser is hand-written
16//!
17//! Upstream's parser is Python's `json`, which differs from `serde_json` in two
18//! ways that this module has to reproduce:
19//!
20//! * `json.loads` accepts (and `json.dumps(allow_nan=True)` emits) the
21//!   non-finite literals `NaN`, `Infinity` and `-Infinity`. `serde_json` has no
22//!   `Value` that can hold them and rejects the documents outright.
23//! * `serde_json` caps nesting at 128 levels, so a 200-deep document — which
24//!   CPython parses without complaint — came back as "invalid JSON".
25//!
26//! Raising a recursion limit only moves the failure to a stack overflow, so
27//! parsing, rendering and *dropping* the tree here are all iterative: nesting
28//! depth costs heap, never stack, and no document can crash the process. Scalar
29//! *decoding* is still delegated to `serde_json`, so string escapes and number
30//! formatting stay exactly as they were.
31//!
32//! That leaves nesting *unbounded* where CPython eventually raises
33//! `RecursionError` — somewhere past 10 000 levels, at a depth that depends on
34//! the interpreter's C stack rather than on anything in the format. Reproducing
35//! a number that moves between machines would be a made-up divergence of its
36//! own, so this accepts every document CPython would and then some.
37
38use std::collections::HashMap;
39
40use crate::console::{Console, ConsoleOptions};
41use crate::errors::{Result, RichError};
42use crate::protocol::Renderable;
43use crate::segment::Segment;
44use crate::style::Style;
45
46/// A parsed JSON document, rendered with syntax highlighting. Mirrors `rich.json.JSON`.
47pub struct Json {
48    value: Node,
49    styles: JsonStyles,
50    /// See [`Json::no_wrap`].
51    no_wrap: bool,
52}
53
54/// A parsed JSON value.
55///
56/// Scalars keep the form they will be printed in: numbers are stored already
57/// formatted by `serde_json`, strings already decoded (upstream re-encodes them
58/// through `json.dumps`, so `"A"` prints as `"A"`).
59#[derive(Debug)]
60enum Node {
61    Null,
62    Bool(bool),
63    Number(String),
64    /// `NaN`, `Infinity` or `-Infinity`. Python's `json` round-trips these;
65    /// rich's `JSONHighlighter` has no rule that matches them, so they print
66    /// unstyled.
67    NonFinite(&'static str),
68    Str(String),
69    Array(Vec<Node>),
70    Object(Vec<(String, Node)>),
71}
72
73impl Drop for Node {
74    /// Dismantle the tree with an explicit stack.
75    ///
76    /// The compiler's drop glue recurses once per nesting level, so a document
77    /// deep enough to parse (parsing has no depth limit here) would overflow
78    /// the stack on the way out — a crash with no error message at all, which
79    /// is worse than the rejection this module used to hand out.
80    fn drop(&mut self) {
81        let mut pending: Vec<Node> = Vec::new();
82        take_children(self, &mut pending);
83        while let Some(mut node) = pending.pop() {
84            take_children(&mut node, &mut pending);
85            // `node` drops here with its children already moved out, so this
86            // same `drop` runs against an empty container and stops.
87        }
88    }
89}
90
91/// Move a node's children into `out`, leaving the node childless.
92fn take_children(node: &mut Node, out: &mut Vec<Node>) {
93    match node {
94        Node::Array(items) => out.append(items),
95        Node::Object(entries) => out.extend(entries.drain(..).map(|(_, value)| value)),
96        _ => {}
97    }
98}
99
100struct JsonStyles {
101    brace: Style,
102    key: Style,
103    string: Style,
104    number: Style,
105    bool_true: Style,
106    bool_false: Style,
107    null: Style,
108}
109
110impl JsonStyles {
111    fn defaults() -> Self {
112        let s = |spec: &str| Style::parse(spec).expect("valid built-in style");
113        JsonStyles {
114            brace: s("bold"),
115            key: s("bold blue"),
116            string: s("green"),
117            number: s("bold cyan"),
118            bool_true: s("italic bright_green"),
119            bool_false: s("italic bright_red"),
120            null: s("italic magenta"),
121        }
122    }
123}
124
125/// One entry of the render work list, consumed newest-first.
126enum Task<'a> {
127    /// Render this value indented `usize` levels deep.
128    Value(&'a Node, usize),
129    /// Emit a segment that has already been decided.
130    Emit(Segment),
131}
132
133impl Json {
134    /// Parse `text` as JSON. Returns an error if it is not valid JSON.
135    pub fn new(text: &str) -> Result<Self> {
136        Ok(Json {
137            value: Parser::new(text).parse_document()?,
138            styles: JsonStyles::defaults(),
139            no_wrap: false,
140        })
141    }
142
143    /// Keep `rich.json.JSON`'s `self.text.no_wrap = True`, which **crops** each
144    /// line at the available width instead of wrapping it.
145    ///
146    /// Defaults to `false`, because that is what a *top-level*
147    /// `Console.print(JSON(...))` produces and that is how this renderable is
148    /// normally reached. Upstream's flag really is set, but `Console.print`
149    /// never renders the `Text` it is set on: `_collect_renderables` sees a
150    /// `Text`, buffers it, and `check_text` hands back
151    /// `Text(sep, justify=…, end=…).join(buffered)` — and `Text.join` starts
152    /// from `self.blank_copy()`, i.e. from the *separator's* metadata. The
153    /// separator has `no_wrap=None`, so the copy that actually renders wraps
154    /// with the default `fold` overflow.
155    ///
156    /// Turn it on whenever the document is **nested** inside another
157    /// renderable — a `Panel`, `Padding`, `Styled`, `Constrain`, or rich-cli's
158    /// `ForceWidth`. Those are `ConsoleRenderable`s, so `_collect_renderables`
159    /// appends them untouched and the inner `Text` reaches
160    /// `Text.__rich_console__` with the flag intact; `Text.wrap` then skips
161    /// `divide_line` and only `truncate`s to the width.
162    ///
163    /// The two are not interchangeable. Wrapping keeps every character;
164    /// cropping discards what does not fit, which for JSON means the printed
165    /// document no longer parses — so the choice has to follow upstream's,
166    /// not taste.
167    #[must_use]
168    pub fn no_wrap(mut self, no_wrap: bool) -> Self {
169        self.no_wrap = no_wrap;
170        self
171    }
172
173    /// Flatten the document into segments, iteratively.
174    ///
175    /// A recursive walk would descend once per nesting level and overflow the
176    /// stack on the deep documents the parser now accepts.
177    fn render_value(&self) -> Vec<Segment> {
178        let brace = |text: &str| Segment::new(text.to_string(), Some(self.styles.brace.clone()));
179        let plain = |text: String| Segment::new(text, None);
180
181        let mut out = Vec::new();
182        let mut stack = vec![Task::Value(&self.value, 0)];
183        while let Some(task) = stack.pop() {
184            let (node, level) = match task {
185                Task::Emit(segment) => {
186                    out.push(segment);
187                    continue;
188                }
189                Task::Value(node, level) => (node, level),
190            };
191            match node {
192                Node::Null => out.push(Segment::new(
193                    "null".to_string(),
194                    Some(self.styles.null.clone()),
195                )),
196                Node::Bool(true) => out.push(Segment::new(
197                    "true".to_string(),
198                    Some(self.styles.bool_true.clone()),
199                )),
200                Node::Bool(false) => out.push(Segment::new(
201                    "false".to_string(),
202                    Some(self.styles.bool_false.clone()),
203                )),
204                Node::Number(number) => out.push(Segment::new(
205                    number.clone(),
206                    Some(self.styles.number.clone()),
207                )),
208                Node::NonFinite(literal) => out.push(plain((*literal).to_string())),
209                Node::Str(string) => out.push(Segment::new(
210                    quote(string),
211                    Some(self.styles.string.clone()),
212                )),
213                Node::Array(items) => {
214                    out.push(brace("["));
215                    if items.is_empty() {
216                        out.push(brace("]"));
217                        continue;
218                    }
219                    out.push(plain("\n".to_string()));
220                    // Pushed back-to-front, so they pop in document order.
221                    stack.push(Task::Emit(brace("]")));
222                    stack.push(Task::Emit(plain("  ".repeat(level))));
223                    let last = items.len() - 1;
224                    for (index, item) in items.iter().enumerate().rev() {
225                        stack.push(Task::Emit(plain("\n".to_string())));
226                        if index != last {
227                            stack.push(Task::Emit(plain(",".to_string())));
228                        }
229                        stack.push(Task::Value(item, level + 1));
230                        stack.push(Task::Emit(plain("  ".repeat(level + 1))));
231                    }
232                }
233                Node::Object(entries) => {
234                    out.push(brace("{"));
235                    if entries.is_empty() {
236                        out.push(brace("}"));
237                        continue;
238                    }
239                    out.push(plain("\n".to_string()));
240                    stack.push(Task::Emit(brace("}")));
241                    stack.push(Task::Emit(plain("  ".repeat(level))));
242                    let last = entries.len() - 1;
243                    for (index, (key, item)) in entries.iter().enumerate().rev() {
244                        stack.push(Task::Emit(plain("\n".to_string())));
245                        if index != last {
246                            stack.push(Task::Emit(plain(",".to_string())));
247                        }
248                        stack.push(Task::Value(item, level + 1));
249                        stack.push(Task::Emit(plain(": ".to_string())));
250                        stack.push(Task::Emit(Segment::new(
251                            quote(key),
252                            Some(self.styles.key.clone()),
253                        )));
254                        stack.push(Task::Emit(plain("  ".repeat(level + 1))));
255                    }
256                }
257            }
258        }
259        out
260    }
261}
262
263impl Renderable for Json {
264    fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
265        let segments = self.render_value();
266        if self.no_wrap {
267            // `Text.wrap` with `no_wrap` keeps the line whole and then calls
268            // `line.truncate(width, overflow="fold")`, which is a crop. See
269            // [`Json::no_wrap`] for when upstream gets here.
270            Segment::crop_lines(&segments, options.max_width)
271        } else {
272            // The break must land at a *word* boundary: the joined copy carries
273            // no overflow either, so upstream wraps with the default `fold`
274            // overflow and only splits mid-word when a single token is wider
275            // than the line.
276            Segment::fold_lines_words(&segments, options.max_width)
277        }
278    }
279}
280
281/// Serialize a string as a JSON string literal (quoted + escaped).
282fn quote(string: &str) -> String {
283    serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
284}
285
286/// A container being filled in, held on the parser's explicit stack.
287enum Frame {
288    Array(Vec<Node>),
289    Object {
290        entries: Vec<(String, Node)>,
291        /// Key -> position in `entries`, so a repeated key overwrites in place
292        /// (`{"a": 1, "a": 2}` is one entry) without an O(n^2) rescan. Dropped
293        /// with the frame, so only the objects on the current path pay for it.
294        seen: HashMap<String, usize>,
295        /// The key whose value is currently being parsed.
296        key: String,
297    },
298}
299
300/// A non-recursive JSON reader. Structure is walked with an explicit stack;
301/// scalar tokens are handed to `serde_json` for decoding so escapes, number
302/// formats and their rejections stay identical to the rest of the workspace.
303struct Parser<'a> {
304    src: &'a str,
305    bytes: &'a [u8],
306    pos: usize,
307}
308
309impl<'a> Parser<'a> {
310    fn new(src: &'a str) -> Self {
311        Parser {
312            src,
313            bytes: src.as_bytes(),
314            pos: 0,
315        }
316    }
317
318    fn parse_document(&mut self) -> Result<Node> {
319        let value = self.parse_value()?;
320        self.skip_whitespace();
321        if self.pos != self.bytes.len() {
322            return Err(self.error("trailing characters"));
323        }
324        Ok(value)
325    }
326
327    /// Parse one value, descending into containers with an explicit stack.
328    fn parse_value(&mut self) -> Result<Node> {
329        let mut stack: Vec<Frame> = Vec::new();
330        let mut node: Node;
331
332        'descend: loop {
333            self.skip_whitespace();
334            match self.peek() {
335                Some(b'[') => {
336                    self.pos += 1;
337                    self.skip_whitespace();
338                    if self.peek() == Some(b']') {
339                        self.pos += 1;
340                        node = Node::Array(Vec::new());
341                    } else {
342                        stack.push(Frame::Array(Vec::new()));
343                        continue 'descend;
344                    }
345                }
346                Some(b'{') => {
347                    self.pos += 1;
348                    self.skip_whitespace();
349                    if self.peek() == Some(b'}') {
350                        self.pos += 1;
351                        node = Node::Object(Vec::new());
352                    } else {
353                        let key = self.parse_key()?;
354                        stack.push(Frame::Object {
355                            entries: Vec::new(),
356                            seen: HashMap::new(),
357                            key,
358                        });
359                        continue 'descend;
360                    }
361                }
362                _ => node = self.parse_scalar()?,
363            }
364
365            // `node` is finished: hand it to its parent, then close as many
366            // containers as end here.
367            loop {
368                let Some(frame) = stack.last_mut() else {
369                    return Ok(node);
370                };
371                let closing = match frame {
372                    Frame::Array(items) => {
373                        items.push(node);
374                        b']'
375                    }
376                    Frame::Object { entries, seen, key } => {
377                        let key = std::mem::take(key);
378                        match seen.get(&key) {
379                            Some(&at) => entries[at].1 = node,
380                            None => {
381                                seen.insert(key.clone(), entries.len());
382                                entries.push((key, node));
383                            }
384                        }
385                        b'}'
386                    }
387                };
388                self.skip_whitespace();
389                match self.peek() {
390                    Some(b',') => {
391                        self.pos += 1;
392                        if closing == b'}' {
393                            let next_key = self.parse_key()?;
394                            if let Some(Frame::Object { key, .. }) = stack.last_mut() {
395                                *key = next_key;
396                            }
397                        }
398                        continue 'descend;
399                    }
400                    Some(byte) if byte == closing => {
401                        self.pos += 1;
402                        node = match stack.pop() {
403                            Some(Frame::Array(items)) => Node::Array(items),
404                            Some(Frame::Object { entries, .. }) => Node::Object(entries),
405                            None => unreachable!("the frame was just borrowed"),
406                        };
407                    }
408                    _ if closing == b']' => return Err(self.error("expected `,` or `]`")),
409                    _ => return Err(self.error("expected `,` or `}`")),
410                }
411            }
412        }
413    }
414
415    /// Parse `"key" :`, leaving the parser on the value.
416    fn parse_key(&mut self) -> Result<String> {
417        self.skip_whitespace();
418        if self.peek() != Some(b'"') {
419            return Err(self.error("key must be a string"));
420        }
421        let key = self.parse_string()?;
422        self.skip_whitespace();
423        if self.peek() != Some(b':') {
424            return Err(self.error("expected `:`"));
425        }
426        self.pos += 1;
427        Ok(key)
428    }
429
430    fn parse_scalar(&mut self) -> Result<Node> {
431        match self.peek() {
432            Some(b'"') => Ok(Node::Str(self.parse_string()?)),
433            Some(b't') => {
434                self.expect_literal("true")?;
435                Ok(Node::Bool(true))
436            }
437            Some(b'f') => {
438                self.expect_literal("false")?;
439                Ok(Node::Bool(false))
440            }
441            Some(b'n') => {
442                self.expect_literal("null")?;
443                Ok(Node::Null)
444            }
445            // Python's json emits and accepts these three (`allow_nan=True` is
446            // the default both ways), so upstream renders documents containing
447            // them instead of rejecting the file.
448            Some(b'N') => {
449                self.expect_literal("NaN")?;
450                Ok(Node::NonFinite("NaN"))
451            }
452            Some(b'I') => {
453                self.expect_literal("Infinity")?;
454                Ok(Node::NonFinite("Infinity"))
455            }
456            Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
457                self.pos += "-Infinity".len();
458                Ok(Node::NonFinite("-Infinity"))
459            }
460            Some(b'-' | b'0'..=b'9') => self.parse_number(),
461            Some(_) => Err(self.error("expected value")),
462            None => Err(self.error("EOF while parsing a value")),
463        }
464    }
465
466    /// Read a string token and decode it with `serde_json`, so escapes, lone
467    /// surrogates and raw control characters behave exactly as before.
468    fn parse_string(&mut self) -> Result<String> {
469        let start = self.pos;
470        let mut end = self.pos + 1;
471        loop {
472            match self.bytes.get(end) {
473                None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
474                Some(b'"') => {
475                    end += 1;
476                    break;
477                }
478                Some(b'\\') => {
479                    end += 1;
480                    // Step over the escaped character whole. A multi-byte
481                    // character after a backslash is invalid JSON, but `end`
482                    // must still land on a UTF-8 boundary or slicing panics
483                    // before `serde_json` gets to reject it.
484                    match self.src[end..].chars().next() {
485                        Some(ch) => end += ch.len_utf8(),
486                        None => {
487                            return Err(
488                                self.error_at(self.bytes.len(), "EOF while parsing a string")
489                            )
490                        }
491                    }
492                }
493                // Continuation bytes are never `"` or `\`, so scanning byte by
494                // byte cannot mistake one for a delimiter.
495                Some(_) => end += 1,
496            }
497        }
498        let decoded: String = serde_json::from_str(&self.src[start..end])
499            .map_err(|error| self.error_at(start, &describe(&error)))?;
500        self.pos = end;
501        Ok(decoded)
502    }
503
504    /// Read a number token and let `serde_json` decide whether it is one — this
505    /// keeps `float_roundtrip` parsing and `Number`'s formatting.
506    fn parse_number(&mut self) -> Result<Node> {
507        let start = self.pos;
508        let mut end = self.pos;
509        while matches!(
510            self.bytes.get(end),
511            Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
512        ) {
513            end += 1;
514        }
515        let number: serde_json::Number = serde_json::from_str(&self.src[start..end])
516            .map_err(|error| self.error_at(start, &describe(&error)))?;
517        self.pos = end;
518        Ok(Node::Number(number.to_string()))
519    }
520
521    fn expect_literal(&mut self, literal: &str) -> Result<()> {
522        if self.src[self.pos..].starts_with(literal) {
523            self.pos += literal.len();
524            Ok(())
525        } else {
526            Err(self.error("expected value"))
527        }
528    }
529
530    fn peek(&self) -> Option<u8> {
531        self.bytes.get(self.pos).copied()
532    }
533
534    fn skip_whitespace(&mut self) {
535        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
536            self.pos += 1;
537        }
538    }
539
540    fn error(&self, message: &str) -> RichError {
541        self.error_at(self.pos, message)
542    }
543
544    fn error_at(&self, pos: usize, message: &str) -> RichError {
545        let (line, column) = self.line_column(pos);
546        RichError::Json(format!("{message} at line {line} column {column}"))
547    }
548
549    fn line_column(&self, pos: usize) -> (usize, usize) {
550        let mut pos = pos.min(self.src.len());
551        while !self.src.is_char_boundary(pos) {
552            pos -= 1;
553        }
554        let before = &self.src[..pos];
555        let line = 1 + before.matches('\n').count();
556        let column = before
557            .rsplit('\n')
558            .next()
559            .map_or(0, |tail| tail.chars().count())
560            + 1;
561        (line, column)
562    }
563}
564
565/// `serde_json`'s message without its own `at line … column …` suffix, which
566/// counts from the start of the token slice rather than the document.
567fn describe(error: &serde_json::Error) -> String {
568    let text = error.to_string();
569    match text.find(" at line ") {
570        Some(at) => text[..at].to_string(),
571        None => text,
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use crate::color::ColorSystem;
579
580    fn render(text: &str) -> String {
581        let console = Console::builder()
582            .force_terminal(true)
583            .color_system(Some(ColorSystem::Truecolor))
584            .width(40)
585            .build();
586        console.render_to_string(&Json::new(text).unwrap())
587    }
588
589    fn render_plain(text: &str, width: usize) -> String {
590        let console = Console::builder().width(width).no_color(true).build();
591        console.render_to_string(&Json::new(text).expect("valid json"))
592    }
593
594    #[test]
595    fn empty_collections_stay_inline() {
596        assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
597        assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
598    }
599
600    #[test]
601    fn object_with_scalars() {
602        assert_eq!(
603            render(r#"{"ok": false}"#),
604            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
605        );
606    }
607
608    #[test]
609    fn invalid_json_errors() {
610        assert!(Json::new("{not json}").is_err());
611    }
612
613    #[test]
614    fn non_ascii_stays_utf8_in_input_order() {
615        // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
616        // characters render as UTF-8 (not \uXXXX), and keys keep input order.
617        // (Byte-parity is guaranteed by the `json_unicode` golden.)
618        let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
619        assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
620        assert!(out.contains('\u{2764}'), "heart stays UTF-8");
621        let name_at = out.find("name").expect("name key present");
622        let emoji_at = out.find("emoji").expect("emoji key present");
623        assert!(name_at < emoji_at, "keys keep input order");
624    }
625
626    #[test]
627    fn a_long_value_is_wrapped_rather_than_cropped() {
628        // A long string value used to be cut mid-token, so the printed document
629        // was missing data -- and, for JSON, no longer parseable -- at exit 0.
630        let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
631        let out = render_plain(&payload, 40);
632        assert_eq!(
633            out.matches('y').count(),
634            120,
635            "characters were dropped:
636{out}"
637        );
638    }
639
640    /// Upstream wraps the JSON at word boundaries: `JSON.text` asks for
641    /// `no_wrap`, but `Console.print` re-joins it through `Text(sep).join(...)`
642    /// and the joined copy carries neither the flag nor an overflow, so the
643    /// default `fold` word wrap applies. Character folding split words
644    /// (`over t` / `he lazy`), which no upstream output ever shows.
645    ///
646    /// Captured from rich 15.0.0:
647    /// `Console(width=40).print(JSON(...))`.
648    #[test]
649    fn wrapping_breaks_at_word_boundaries() {
650        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
651        assert_eq!(
652            render_plain(payload, 40),
653            "{\n  \"k\": \"the quick brown fox jumps over \n\
654             the lazy dog and keeps running for a \n\
655             very long time indeed\"\n}"
656        );
657    }
658
659    /// Nested inside another renderable, `JSON.text.no_wrap` survives and each
660    /// line is **cropped** at the width rather than wrapped — see
661    /// [`Json::no_wrap`]. Wrapping here instead was silent content loss: with
662    /// `rich -j doc.json -w 120` on an 80-column console the document was laid
663    /// out at 120 and then cropped to 80 by `Console.print`, so whole runs
664    /// vanished and the surviving text read as if it were contiguous.
665    ///
666    /// Captured from rich-cli 1.8.1 driven by rich 15.0.0:
667    /// `COLUMNS=80 rich -j long.json -w 40`.
668    #[test]
669    fn a_nested_document_is_cropped_rather_than_wrapped() {
670        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
671        let console = Console::builder().width(40).no_color(true).build();
672        let json = Json::new(payload).expect("valid json").no_wrap(true);
673        assert_eq!(
674            console.render_to_string(&json),
675            "{\n  \"k\": \"the quick brown fox jumps over t\n}"
676        );
677
678        // …and the wrap is still the default, because a bare
679        // `Console.print(JSON(...))` loses the flag in `Text.join`.
680        assert_eq!(
681            render_plain(payload, 40),
682            "{\n  \"k\": \"the quick brown fox jumps over \n\
683             the lazy dog and keeps running for a \n\
684             very long time indeed\"\n}"
685        );
686    }
687
688    /// A crop must not cut a double-width character in half: `set_cell_size`
689    /// drops the straddling character and the line comes out one cell short,
690    /// never one cell over.
691    #[test]
692    fn cropping_never_splits_a_wide_character() {
693        let console = Console::builder().width(12).no_color(true).build();
694        let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
695            .expect("valid json")
696            .no_wrap(true);
697        for line in console.render_to_string(&json).lines() {
698            assert!(
699                crate::cells::cell_len(line) <= 12,
700                "line {line:?} overflows the crop"
701            );
702        }
703    }
704
705    /// serde_json's default float parser takes a fast path that can land 1 ULP
706    /// from the value in the file, so the rendered number parsed back to a
707    /// *different* double. The `float_roundtrip` feature makes parsing exact.
708    #[test]
709    fn floats_round_trip_exactly() {
710        for literal in [
711            "-938371.9565467801",
712            "0.1",
713            "1.7976931348623157e308",
714            "5e-324",
715            "3.141592653589793",
716        ] {
717            let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
718            let rendered: String = out
719                .split(':')
720                .nth(1)
721                .expect("a value after the key")
722                .trim()
723                .trim_end_matches(['}', ' ', '\n'])
724                .to_string();
725            let want: f64 = literal.parse().expect("literal parses");
726            let got: f64 = rendered
727                .parse()
728                .unwrap_or_else(|_| panic!("rendered {rendered:?}"));
729            assert_eq!(
730                got.to_bits(),
731                want.to_bits(),
732                "{literal} rendered as {rendered} — a different double"
733            );
734        }
735    }
736
737    /// serde_json stops at 128 levels, so a 200-deep document — which CPython
738    /// parses without complaint — was reported as invalid JSON and the CLI
739    /// exited 1 on a file upstream renders.
740    #[test]
741    fn deep_nesting_is_not_rejected() {
742        for depth in [128, 129, 200, 1000] {
743            let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
744            let json = Json::new(&payload)
745                .unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
746            let console = Console::builder()
747                .width(4 * depth + 8)
748                .no_color(true)
749                .build();
750            let out = console.render_to_string(&json);
751            assert_eq!(
752                out.matches('[').count(),
753                depth,
754                "depth {depth} did not render every level"
755            );
756        }
757    }
758
759    /// Parsing and dropping must cost heap, not stack: a recursive parser (or
760    /// the compiler's recursive drop glue) turns a deep document into a stack
761    /// overflow, which kills the process without even an error message.
762    #[test]
763    fn very_deep_nesting_does_not_overflow_the_stack() {
764        let depth = 100_000;
765        let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
766        let json = Json::new(&payload).expect("deep document parses");
767        drop(json);
768    }
769
770    /// Python's json accepts and emits `NaN` / `Infinity` / `-Infinity`
771    /// (`allow_nan=True` is the default), and rich's JSONHighlighter has no
772    /// rule that matches them, so upstream prints them *unstyled*. serde_json
773    /// rejected the whole document.
774    ///
775    /// Captured from rich 15.0.0:
776    /// `Console(width=40, force_terminal=True).print(JSON(...))`.
777    #[test]
778    fn non_finite_numbers_render_unstyled() {
779        assert_eq!(
780            render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
781            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"a\"\x1b[0m: NaN,\n  \x1b[1;34m\"b\"\x1b[0m: \
782             Infinity,\n  \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n  \x1b[1;34m\"d\"\x1b[0m: \
783             \x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
784        );
785        // Python spells them with those exact capitalisations and nothing else.
786        for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
787            assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
788        }
789    }
790
791    /// The hand-written reader must accept and reject exactly what serde_json
792    /// does — apart from the three Python constants it exists to add.
793    #[test]
794    fn acceptance_matches_serde_json() {
795        let samples = [
796            "{}",
797            "[]",
798            "  {\t\"a\" :\n1 }  ",
799            r#"{"a": 1, "a": 2}"#,
800            r#"{"a": [1, {"b": null}], "c": "x"}"#,
801            "0",
802            "-0",
803            "0.0",
804            "1e10",
805            "1E+10",
806            "1e-7",
807            "1e999",
808            "12345678901234567890",
809            "-12345678901234567890123456789012345",
810            "01",
811            "1.",
812            ".1",
813            "+1",
814            "1e",
815            "-",
816            "--1",
817            "-i",
818            "-Inf",
819            "Infinit",
820            "NAN",
821            "1 2",
822            "",
823            "   ",
824            "{",
825            "[",
826            "]",
827            "}",
828            "[,]",
829            "[1,]",
830            r#"{"a": 1,}"#,
831            r#"{a: 1}"#,
832            r#"{'a': 1}"#,
833            r#"{"a" 1}"#,
834            "[1 2]",
835            "truex",
836            "tru",
837            "nul",
838            r#""unterminated"#,
839            r#""\q""#,
840            r#""é""#,
841            r#""😀""#,
842            r#""\ud800""#,
843            "\"raw\nnewline\"",
844            r#""café ❤""#,
845            "\"\u{e9}\\\"",
846            "\u{feff}{}",
847            "[[[[1]]]]",
848        ];
849        for sample in samples {
850            let ours = Json::new(sample).is_ok();
851            let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
852            assert_eq!(ours, theirs, "disagreed about {sample:?}");
853        }
854    }
855
856    /// And the tree it builds must be the tree serde_json would have built.
857    /// `serde_json::to_string_pretty` happens to use the very layout upstream's
858    /// `json.dumps(indent=2)` does, so it doubles as a reference dump: key
859    /// order, repeated-key collapsing, string escaping and number formatting
860    /// all have to agree.
861    #[test]
862    fn the_parsed_tree_matches_serde_json() {
863        let samples = [
864            r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
865            r#"{"a": 1, "b": 2, "a": 3}"#,
866            r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
867            r#"{"k": "A\t\"x\"A\\\/é"}"#,
868            r#"[0, -0.5, 1e10, 1E+10, 1e-7, 12345678901234567890, 1.7976931348623157e308]"#,
869            r#"{"café": "❤", "": ""}"#,
870            "[]",
871            "{}",
872            "\"top level\"",
873            "1234",
874        ];
875        for sample in samples {
876            let reference: serde_json::Value =
877                serde_json::from_str(sample).expect("sample is valid JSON");
878            assert_eq!(
879                render_plain(sample, 10_000),
880                serde_json::to_string_pretty(&reference).expect("value re-serialises"),
881                "diverged on {sample}"
882            );
883        }
884    }
885
886    /// A repeated key collapses to one entry — first position, last value —
887    /// which is what both `dict` and serde_json's `preserve_order` produce.
888    #[test]
889    fn a_repeated_key_keeps_its_position_and_last_value() {
890        assert_eq!(
891            render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
892            "{\n  \"a\": 3,\n  \"b\": 2\n}"
893        );
894    }
895
896    /// Escapes are decoded and re-encoded, because upstream re-serialises the
897    /// parsed data with `json.dumps`.
898    #[test]
899    fn escapes_are_re_encoded_like_dumps() {
900        assert_eq!(
901            render_plain(r#"{"k": "A\t\"x\""}"#, 60),
902            "{\n  \"k\": \"A\\t\\\"x\\\"\"\n}"
903        );
904    }
905}