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`) 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//! important ways this module reproduces:
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. String decoding and finite floating-point
29//! formatting use `serde_json`; integer tokens retain their exact digits and
30//! overflowing exponents become signed Infinity as in Python.
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::measure::Measurement;
43use crate::protocol::Renderable;
44use crate::segment::Segment;
45use crate::style::Style;
46
47/// A parsed JSON document, rendered with syntax highlighting. Mirrors `rich.json.JSON`.
48pub struct Json {
49    value: Node,
50    styles: JsonStyles,
51    /// See [`Json::no_wrap`].
52    no_wrap: bool,
53    #[cfg(feature = "json-escape-safe")]
54    escape_safe: bool,
55}
56
57/// A parsed JSON value.
58///
59/// Scalars keep the form they will be printed in: numbers are stored already
60/// normalized where needed, strings already decoded (upstream re-encodes them
61/// through `json.dumps`, so `"A"` prints as `"A"`).
62#[derive(Debug)]
63enum Node {
64    Null,
65    Bool(bool),
66    Number(String),
67    /// `NaN`, `Infinity` or `-Infinity`. Python's `json` round-trips these;
68    /// rich's `JSONHighlighter` has no rule that matches them, so they print
69    /// unstyled.
70    NonFinite(&'static str),
71    Str(String),
72    Array(Vec<Node>),
73    Object(Vec<(String, Node)>),
74}
75
76impl Drop for Node {
77    /// Dismantle the tree with an explicit stack.
78    ///
79    /// The compiler's drop glue recurses once per nesting level, so a document
80    /// deep enough to parse (parsing has no depth limit here) would overflow
81    /// the stack on the way out — a crash with no error message at all, which
82    /// is worse than the rejection this module used to hand out.
83    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            // `node` drops here with its children already moved out, so this
89            // same `drop` runs against an empty container and stops.
90        }
91    }
92}
93
94/// Move a node's children into `out`, leaving the node childless.
95fn 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
128/// One entry of the render work list, consumed newest-first.
129enum Task<'a> {
130    /// Render this value indented `usize` levels deep.
131    Value(&'a Node, usize),
132    /// Emit a segment that has already been decided.
133    Emit(Segment),
134}
135
136impl Json {
137    /// Parse `text` as JSON. Returns an error if it is not valid JSON.
138    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    /// Opt in to escape-aware display boundaries (requires `json-escape-safe`).
149    /// Cropping omits partial escapes. Folding preserves escapes that fit the
150    /// width; narrower widths split oversized escapes to avoid losing content.
151    /// The default remains Python rich's ordinary word folding/cropping.
152    #[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    /// Keep `rich.json.JSON`'s `self.text.no_wrap = True`, which **crops** each
159    /// line at the available width instead of wrapping it.
160    ///
161    /// Defaults to `false`, because that is what a *top-level*
162    /// `Console.print(JSON(...))` produces and that is how this renderable is
163    /// normally reached. Upstream's flag really is set, but `Console.print`
164    /// never renders the `Text` it is set on: `_collect_renderables` sees a
165    /// `Text`, buffers it, and `check_text` hands back
166    /// `Text(sep, justify=…, end=…).join(buffered)` — and `Text.join` starts
167    /// from `self.blank_copy()`, i.e. from the *separator's* metadata. The
168    /// separator has `no_wrap=None`, so the copy that actually renders wraps
169    /// with the default `fold` overflow.
170    ///
171    /// Turn it on whenever the document is **nested** inside another
172    /// renderable — a `Panel`, `Padding`, `Styled`, `Constrain`, or rich-cli's
173    /// `ForceWidth`. Those are `ConsoleRenderable`s, so `_collect_renderables`
174    /// appends them untouched and the inner `Text` reaches
175    /// `Text.__rich_console__` with the flag intact; `Text.wrap` then skips
176    /// `divide_line` and only `truncate`s to the width.
177    ///
178    /// The two are not interchangeable. Wrapping keeps every character;
179    /// cropping discards what does not fit, which for JSON means the printed
180    /// document no longer parses — so the choice has to follow upstream's,
181    /// not taste.
182    #[must_use]
183    pub fn no_wrap(mut self, no_wrap: bool) -> Self {
184        self.no_wrap = no_wrap;
185        self
186    }
187
188    /// Flatten the document into segments, iteratively.
189    ///
190    /// A recursive walk would descend once per nesting level and overflow the
191    /// stack on the deep documents the parser now accepts.
192    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                    // Pushed back-to-front, so they pop in document order.
236                    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    /// Upstream `JSON.__rich__` returns its highlighted `Text`, so measuring a
280    /// `JSON` is `Text.__rich_measure__` over the formatted document.
281    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            // `Text.wrap` with `no_wrap` keeps the line whole and then calls
299            // `line.truncate(width, overflow="fold")`, which is a crop. See
300            // [`Json::no_wrap`] for when upstream gets here.
301            Segment::crop_lines(&segments, options.max_width)
302        } else {
303            // The break must land at a *word* boundary: the joined copy carries
304            // no overflow either, so upstream wraps with the default `fold`
305            // overflow and only splits mid-word when a single token is wider
306            // than the line.
307            Segment::fold_lines_words(&segments, options.max_width)
308        }
309    }
310}
311
312/// Tokenize each physical line into JSON escapes and ordinary graphemes, then
313/// choose every boundary from the space actually remaining. No stale absolute
314/// wrap points survive an adjusted escape boundary (#98).
315#[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        // Keep upstream's exact behavior when there is no escape to protect.
330        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                        // An atom wider than the whole console cannot both fit
356                        // and stay atomic. Split its ASCII spelling rather than
357                        // overrun into Console's final crop and lose characters.
358                        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
424/// Serialize a string as a JSON string literal (quoted + escaped).
425fn quote(string: &str) -> String {
426    serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
427}
428
429/// A container being filled in, held on the parser's explicit stack.
430enum Frame {
431    Array(Vec<Node>),
432    Object {
433        entries: Vec<(String, Node)>,
434        /// Key -> position in `entries`, so a repeated key overwrites in place
435        /// (`{"a": 1, "a": 2}` is one entry) without an O(n^2) rescan. Dropped
436        /// with the frame, so only the objects on the current path pay for it.
437        seen: HashMap<String, usize>,
438        /// The key whose value is currently being parsed.
439        key: String,
440    },
441}
442
443/// A non-recursive JSON reader. Structure is walked with an explicit stack;
444/// scalar tokens are handed to `serde_json` for decoding so escapes, number
445/// formats and their rejections stay identical to the rest of the workspace.
446struct 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    /// Parse one value, descending into containers with an explicit stack.
471    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            // `node` is finished: hand it to its parent, then close as many
509            // containers as end here.
510            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    /// Parse `"key" :`, leaving the parser on the value.
559    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            // Python's json emits and accepts these three (`allow_nan=True` is
589            // the default both ways), so upstream renders documents containing
590            // them instead of rejecting the file.
591            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    /// Read a string token and decode it with `serde_json`, so escapes, lone
610    /// surrogates and raw control characters behave exactly as before.
611    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                    // Step over the escaped character whole. A multi-byte
624                    // character after a backslash is invalid JSON, but `end`
625                    // must still land on a UTF-8 boundary or slicing panics
626                    // before `serde_json` gets to reject it.
627                    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                // Continuation bytes are never `"` or `\`, so scanning byte by
637                // byte cannot mistake one for a delimiter.
638                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    /// Validate JSON's number grammar before decoding, preserving arbitrary-size
648    /// integers and Python's float overflow to Infinity (#74).
649    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
766/// `serde_json`'s message without its own `at line … column …` suffix, which
767/// counts from the start of the token slice rather than the document.
768fn 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        // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
817        // characters render as UTF-8 (not \uXXXX), and keys keep input order.
818        // (Byte-parity is guaranteed by the `json_unicode` golden.)
819        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        // A long string value used to be cut mid-token, so the printed document
830        // was missing data -- and, for JSON, no longer parseable -- at exit 0.
831        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    /// Upstream wraps the JSON at word boundaries: `JSON.text` asks for
842    /// `no_wrap`, but `Console.print` re-joins it through `Text(sep).join(...)`
843    /// and the joined copy carries neither the flag nor an overflow, so the
844    /// default `fold` word wrap applies. Character folding split words
845    /// (`over t` / `he lazy`), which no upstream output ever shows.
846    ///
847    /// Captured from rich 15.0.0:
848    /// `Console(width=40).print(JSON(...))`.
849    #[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    /// Nested inside another renderable, `JSON.text.no_wrap` survives and each
958    /// line is **cropped** at the width rather than wrapped — see
959    /// [`Json::no_wrap`]. Wrapping here instead was silent content loss: with
960    /// `rich -j doc.json -w 120` on an 80-column console the document was laid
961    /// out at 120 and then cropped to 80 by `Console.print`, so whole runs
962    /// vanished and the surviving text read as if it were contiguous.
963    ///
964    /// Captured from rich-cli 1.8.1 driven by rich 15.0.0:
965    /// `COLUMNS=80 rich -j long.json -w 40`.
966    #[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        // …and the wrap is still the default, because a bare
977        // `Console.print(JSON(...))` loses the flag in `Text.join`.
978        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    /// A crop must not cut a double-width character in half: `set_cell_size`
987    /// drops the straddling character and the line comes out one cell short,
988    /// never one cell over.
989    #[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    /// serde_json's default float parser takes a fast path that can land 1 ULP
1004    /// from the value in the file, so the rendered number parsed back to a
1005    /// *different* double. The `float_roundtrip` feature makes parsing exact.
1006    #[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    /// serde_json stops at 128 levels, so a 200-deep document — which CPython
1036    /// parses without complaint — was reported as invalid JSON and the CLI
1037    /// exited 1 on a file upstream renders.
1038    #[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    /// Parsing and dropping must cost heap, not stack: a recursive parser (or
1058    /// the compiler's recursive drop glue) turns a deep document into a stack
1059    /// overflow, which kills the process without even an error message.
1060    #[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    /// Python's json accepts and emits `NaN` / `Infinity` / `-Infinity`
1069    /// (`allow_nan=True` is the default), and rich's JSONHighlighter has no
1070    /// rule that matches them, so upstream prints them *unstyled*. serde_json
1071    /// rejected the whole document.
1072    ///
1073    /// Captured from rich 15.0.0:
1074    /// `Console(width=40, force_terminal=True).print(JSON(...))`.
1075    #[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        // Python spells them with those exact capitalisations and nothing else.
1084        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    /// The hand-written reader must accept and reject exactly what serde_json
1090    /// does for bounded numbers — Python also accepts overflowing exponents.
1091    #[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    /// And the tree it builds must be the tree serde_json would have built.
1170    /// `serde_json::to_string_pretty` happens to use the very layout upstream's
1171    /// `json.dumps(indent=2)` does, so it doubles as a reference dump: key
1172    /// order, repeated-key collapsing, string escaping and number formatting
1173    /// all have to agree.
1174    #[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    /// A repeated key collapses to one entry — first position, last value —
1200    /// which is what both `dict` and serde_json's `preserve_order` produce.
1201    #[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    /// Escapes are decoded and re-encoded, because upstream re-serialises the
1210    /// parsed data with `json.dumps`.
1211    #[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}