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::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    #[cfg(feature = "json-escape-safe")]
53    escape_safe: bool,
54}
55
56/// A parsed JSON value.
57///
58/// Scalars keep the form they will be printed in: numbers are stored already
59/// normalized where needed, strings already decoded (upstream re-encodes them
60/// through `json.dumps`, so `"A"` prints as `"A"`).
61#[derive(Debug)]
62enum Node {
63    Null,
64    Bool(bool),
65    Number(String),
66    /// `NaN`, `Infinity` or `-Infinity`. Python's `json` round-trips these;
67    /// rich's `JSONHighlighter` has no rule that matches them, so they print
68    /// unstyled.
69    NonFinite(&'static str),
70    Str(String),
71    Array(Vec<Node>),
72    Object(Vec<(String, Node)>),
73}
74
75impl Drop for Node {
76    /// Dismantle the tree with an explicit stack.
77    ///
78    /// The compiler's drop glue recurses once per nesting level, so a document
79    /// deep enough to parse (parsing has no depth limit here) would overflow
80    /// the stack on the way out — a crash with no error message at all, which
81    /// is worse than the rejection this module used to hand out.
82    fn drop(&mut self) {
83        let mut pending: Vec<Node> = Vec::new();
84        take_children(self, &mut pending);
85        while let Some(mut node) = pending.pop() {
86            take_children(&mut node, &mut pending);
87            // `node` drops here with its children already moved out, so this
88            // same `drop` runs against an empty container and stops.
89        }
90    }
91}
92
93/// Move a node's children into `out`, leaving the node childless.
94fn take_children(node: &mut Node, out: &mut Vec<Node>) {
95    match node {
96        Node::Array(items) => out.append(items),
97        Node::Object(entries) => out.extend(entries.drain(..).map(|(_, value)| value)),
98        _ => {}
99    }
100}
101
102struct JsonStyles {
103    brace: Style,
104    key: Style,
105    string: Style,
106    number: Style,
107    bool_true: Style,
108    bool_false: Style,
109    null: Style,
110}
111
112impl JsonStyles {
113    fn defaults() -> Self {
114        let s = |spec: &str| Style::parse(spec).expect("valid built-in style");
115        JsonStyles {
116            brace: s("bold"),
117            key: s("bold blue"),
118            string: s("green"),
119            number: s("bold cyan"),
120            bool_true: s("italic bright_green"),
121            bool_false: s("italic bright_red"),
122            null: s("italic magenta"),
123        }
124    }
125}
126
127/// One entry of the render work list, consumed newest-first.
128enum Task<'a> {
129    /// Render this value indented `usize` levels deep.
130    Value(&'a Node, usize),
131    /// Emit a segment that has already been decided.
132    Emit(Segment),
133}
134
135impl Json {
136    /// Parse `text` as JSON. Returns an error if it is not valid JSON.
137    pub fn new(text: &str) -> Result<Self> {
138        Ok(Json {
139            value: Parser::new(text).parse_document()?,
140            styles: JsonStyles::defaults(),
141            no_wrap: false,
142            #[cfg(feature = "json-escape-safe")]
143            escape_safe: false,
144        })
145    }
146
147    /// Opt in to escape-aware display boundaries (requires `json-escape-safe`).
148    /// Cropping omits partial escapes. Folding preserves escapes that fit the
149    /// width; narrower widths split oversized escapes to avoid losing content.
150    /// The default remains Python rich's ordinary word folding/cropping.
151    #[cfg(feature = "json-escape-safe")]
152    pub fn escape_safe(mut self, enabled: bool) -> Self {
153        self.escape_safe = enabled;
154        self
155    }
156
157    /// Keep `rich.json.JSON`'s `self.text.no_wrap = True`, which **crops** each
158    /// line at the available width instead of wrapping it.
159    ///
160    /// Defaults to `false`, because that is what a *top-level*
161    /// `Console.print(JSON(...))` produces and that is how this renderable is
162    /// normally reached. Upstream's flag really is set, but `Console.print`
163    /// never renders the `Text` it is set on: `_collect_renderables` sees a
164    /// `Text`, buffers it, and `check_text` hands back
165    /// `Text(sep, justify=…, end=…).join(buffered)` — and `Text.join` starts
166    /// from `self.blank_copy()`, i.e. from the *separator's* metadata. The
167    /// separator has `no_wrap=None`, so the copy that actually renders wraps
168    /// with the default `fold` overflow.
169    ///
170    /// Turn it on whenever the document is **nested** inside another
171    /// renderable — a `Panel`, `Padding`, `Styled`, `Constrain`, or rich-cli's
172    /// `ForceWidth`. Those are `ConsoleRenderable`s, so `_collect_renderables`
173    /// appends them untouched and the inner `Text` reaches
174    /// `Text.__rich_console__` with the flag intact; `Text.wrap` then skips
175    /// `divide_line` and only `truncate`s to the width.
176    ///
177    /// The two are not interchangeable. Wrapping keeps every character;
178    /// cropping discards what does not fit, which for JSON means the printed
179    /// document no longer parses — so the choice has to follow upstream's,
180    /// not taste.
181    #[must_use]
182    pub fn no_wrap(mut self, no_wrap: bool) -> Self {
183        self.no_wrap = no_wrap;
184        self
185    }
186
187    /// Flatten the document into segments, iteratively.
188    ///
189    /// A recursive walk would descend once per nesting level and overflow the
190    /// stack on the deep documents the parser now accepts.
191    fn render_value(&self) -> Vec<Segment> {
192        let brace = |text: &str| Segment::new(text.to_string(), Some(self.styles.brace.clone()));
193        let plain = |text: String| Segment::new(text, None);
194
195        let mut out = Vec::new();
196        let mut stack = vec![Task::Value(&self.value, 0)];
197        while let Some(task) = stack.pop() {
198            let (node, level) = match task {
199                Task::Emit(segment) => {
200                    out.push(segment);
201                    continue;
202                }
203                Task::Value(node, level) => (node, level),
204            };
205            match node {
206                Node::Null => out.push(Segment::new(
207                    "null".to_string(),
208                    Some(self.styles.null.clone()),
209                )),
210                Node::Bool(true) => out.push(Segment::new(
211                    "true".to_string(),
212                    Some(self.styles.bool_true.clone()),
213                )),
214                Node::Bool(false) => out.push(Segment::new(
215                    "false".to_string(),
216                    Some(self.styles.bool_false.clone()),
217                )),
218                Node::Number(number) => out.push(Segment::new(
219                    number.clone(),
220                    Some(self.styles.number.clone()),
221                )),
222                Node::NonFinite(literal) => out.push(plain((*literal).to_string())),
223                Node::Str(string) => out.push(Segment::new(
224                    quote(string),
225                    Some(self.styles.string.clone()),
226                )),
227                Node::Array(items) => {
228                    out.push(brace("["));
229                    if items.is_empty() {
230                        out.push(brace("]"));
231                        continue;
232                    }
233                    out.push(plain("\n".to_string()));
234                    // Pushed back-to-front, so they pop in document order.
235                    stack.push(Task::Emit(brace("]")));
236                    stack.push(Task::Emit(plain("  ".repeat(level))));
237                    let last = items.len() - 1;
238                    for (index, item) in items.iter().enumerate().rev() {
239                        stack.push(Task::Emit(plain("\n".to_string())));
240                        if index != last {
241                            stack.push(Task::Emit(plain(",".to_string())));
242                        }
243                        stack.push(Task::Value(item, level + 1));
244                        stack.push(Task::Emit(plain("  ".repeat(level + 1))));
245                    }
246                }
247                Node::Object(entries) => {
248                    out.push(brace("{"));
249                    if entries.is_empty() {
250                        out.push(brace("}"));
251                        continue;
252                    }
253                    out.push(plain("\n".to_string()));
254                    stack.push(Task::Emit(brace("}")));
255                    stack.push(Task::Emit(plain("  ".repeat(level))));
256                    let last = entries.len() - 1;
257                    for (index, (key, item)) in entries.iter().enumerate().rev() {
258                        stack.push(Task::Emit(plain("\n".to_string())));
259                        if index != last {
260                            stack.push(Task::Emit(plain(",".to_string())));
261                        }
262                        stack.push(Task::Value(item, level + 1));
263                        stack.push(Task::Emit(plain(": ".to_string())));
264                        stack.push(Task::Emit(Segment::new(
265                            quote(key),
266                            Some(self.styles.key.clone()),
267                        )));
268                        stack.push(Task::Emit(plain("  ".repeat(level + 1))));
269                    }
270                }
271            }
272        }
273        out
274    }
275}
276
277impl Renderable for Json {
278    fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
279        let segments = self.render_value();
280        #[cfg(feature = "json-escape-safe")]
281        if self.escape_safe {
282            return escape_safe_lines(&segments, options.max_width, self.no_wrap);
283        }
284        if self.no_wrap {
285            // `Text.wrap` with `no_wrap` keeps the line whole and then calls
286            // `line.truncate(width, overflow="fold")`, which is a crop. See
287            // [`Json::no_wrap`] for when upstream gets here.
288            Segment::crop_lines(&segments, options.max_width)
289        } else {
290            // The break must land at a *word* boundary: the joined copy carries
291            // no overflow either, so upstream wraps with the default `fold`
292            // overflow and only splits mid-word when a single token is wider
293            // than the line.
294            Segment::fold_lines_words(&segments, options.max_width)
295        }
296    }
297}
298
299/// Tokenize each physical line into JSON escapes and ordinary graphemes, then
300/// choose every boundary from the space actually remaining. No stale absolute
301/// wrap points survive an adjusted escape boundary (#98).
302#[cfg(feature = "json-escape-safe")]
303fn escape_safe_lines(segments: &[Segment], width: usize, crop: bool) -> Vec<Segment> {
304    if width == 0 {
305        return Vec::new();
306    }
307    let lines = Segment::split_lines(segments);
308    let last = lines.len().saturating_sub(1);
309    let mut out = Vec::new();
310    for (line_index, line) in lines.into_iter().enumerate() {
311        let plain: String = line
312            .iter()
313            .filter(|s| !s.control)
314            .map(|s| s.text.as_str())
315            .collect();
316        // Keep upstream's exact behavior when there is no escape to protect.
317        if !plain.contains('\\') {
318            out.extend(if crop {
319                Segment::crop_lines(&line, width)
320            } else {
321                Segment::fold_lines_words(&line, width)
322            });
323        } else {
324            let (spans, _) = crate::cells::split_graphemes(&plain);
325            let mut atoms = Vec::new();
326            let mut index = 0;
327            while index < spans.len() {
328                let (start, mut end, mut cells) = spans[index];
329                if plain.as_bytes()[start] == b'\\' {
330                    let escape_end = start
331                        + if plain.as_bytes().get(start + 1) == Some(&b'u') {
332                            6
333                        } else {
334                            2
335                        };
336                    while end < escape_end && index + 1 < spans.len() {
337                        index += 1;
338                        end = spans[index].1;
339                        cells += spans[index].2;
340                    }
341                    if !crop && cells > width {
342                        // An atom wider than the whole console cannot both fit
343                        // and stay atomic. Split its ASCII spelling rather than
344                        // overrun into Console's final crop and lose characters.
345                        for offset in start..escape_end {
346                            atoms.push((offset, offset + 1, 1));
347                        }
348                        if end > escape_end {
349                            atoms.push((escape_end, end, 0));
350                        }
351                        index += 1;
352                        continue;
353                    }
354                }
355                atoms.push((start, end, cells));
356                index += 1;
357            }
358            let mut breaks = Vec::new();
359            let mut cells = 0;
360            let mut stop = plain.len();
361            for (start, _, atom_width) in atoms {
362                if cells + atom_width > width {
363                    if crop {
364                        stop = start;
365                        break;
366                    }
367                    if cells > 0 {
368                        breaks.push(start);
369                        cells = 0;
370                    }
371                }
372                cells += atom_width;
373            }
374            let mut position = 0;
375            let mut next = 0;
376            for segment in line {
377                if segment.control {
378                    out.push(segment);
379                    continue;
380                }
381                let mut buffer = String::new();
382                for ch in segment.text.chars() {
383                    if position >= stop {
384                        break;
385                    }
386                    if breaks.get(next) == Some(&position) {
387                        if !buffer.is_empty() {
388                            out.push(Segment::new(
389                                std::mem::take(&mut buffer),
390                                segment.style.clone(),
391                            ));
392                        }
393                        out.push(Segment::line());
394                        next += 1;
395                    }
396                    buffer.push(ch);
397                    position += ch.len_utf8();
398                }
399                if !buffer.is_empty() {
400                    out.push(Segment::new(buffer, segment.style));
401                }
402            }
403        }
404        if line_index != last {
405            out.push(Segment::line());
406        }
407    }
408    out
409}
410
411/// Serialize a string as a JSON string literal (quoted + escaped).
412fn quote(string: &str) -> String {
413    serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
414}
415
416/// A container being filled in, held on the parser's explicit stack.
417enum Frame {
418    Array(Vec<Node>),
419    Object {
420        entries: Vec<(String, Node)>,
421        /// Key -> position in `entries`, so a repeated key overwrites in place
422        /// (`{"a": 1, "a": 2}` is one entry) without an O(n^2) rescan. Dropped
423        /// with the frame, so only the objects on the current path pay for it.
424        seen: HashMap<String, usize>,
425        /// The key whose value is currently being parsed.
426        key: String,
427    },
428}
429
430/// A non-recursive JSON reader. Structure is walked with an explicit stack;
431/// scalar tokens are handed to `serde_json` for decoding so escapes, number
432/// formats and their rejections stay identical to the rest of the workspace.
433struct Parser<'a> {
434    src: &'a str,
435    bytes: &'a [u8],
436    pos: usize,
437}
438
439impl<'a> Parser<'a> {
440    fn new(src: &'a str) -> Self {
441        Parser {
442            src,
443            bytes: src.as_bytes(),
444            pos: 0,
445        }
446    }
447
448    fn parse_document(&mut self) -> Result<Node> {
449        let value = self.parse_value()?;
450        self.skip_whitespace();
451        if self.pos != self.bytes.len() {
452            return Err(self.error("trailing characters"));
453        }
454        Ok(value)
455    }
456
457    /// Parse one value, descending into containers with an explicit stack.
458    fn parse_value(&mut self) -> Result<Node> {
459        let mut stack: Vec<Frame> = Vec::new();
460        let mut node: Node;
461
462        'descend: loop {
463            self.skip_whitespace();
464            match self.peek() {
465                Some(b'[') => {
466                    self.pos += 1;
467                    self.skip_whitespace();
468                    if self.peek() == Some(b']') {
469                        self.pos += 1;
470                        node = Node::Array(Vec::new());
471                    } else {
472                        stack.push(Frame::Array(Vec::new()));
473                        continue 'descend;
474                    }
475                }
476                Some(b'{') => {
477                    self.pos += 1;
478                    self.skip_whitespace();
479                    if self.peek() == Some(b'}') {
480                        self.pos += 1;
481                        node = Node::Object(Vec::new());
482                    } else {
483                        let key = self.parse_key()?;
484                        stack.push(Frame::Object {
485                            entries: Vec::new(),
486                            seen: HashMap::new(),
487                            key,
488                        });
489                        continue 'descend;
490                    }
491                }
492                _ => node = self.parse_scalar()?,
493            }
494
495            // `node` is finished: hand it to its parent, then close as many
496            // containers as end here.
497            loop {
498                let Some(frame) = stack.last_mut() else {
499                    return Ok(node);
500                };
501                let closing = match frame {
502                    Frame::Array(items) => {
503                        items.push(node);
504                        b']'
505                    }
506                    Frame::Object { entries, seen, key } => {
507                        let key = std::mem::take(key);
508                        match seen.get(&key) {
509                            Some(&at) => entries[at].1 = node,
510                            None => {
511                                seen.insert(key.clone(), entries.len());
512                                entries.push((key, node));
513                            }
514                        }
515                        b'}'
516                    }
517                };
518                self.skip_whitespace();
519                match self.peek() {
520                    Some(b',') => {
521                        self.pos += 1;
522                        if closing == b'}' {
523                            let next_key = self.parse_key()?;
524                            if let Some(Frame::Object { key, .. }) = stack.last_mut() {
525                                *key = next_key;
526                            }
527                        }
528                        continue 'descend;
529                    }
530                    Some(byte) if byte == closing => {
531                        self.pos += 1;
532                        node = match stack.pop() {
533                            Some(Frame::Array(items)) => Node::Array(items),
534                            Some(Frame::Object { entries, .. }) => Node::Object(entries),
535                            None => unreachable!("the frame was just borrowed"),
536                        };
537                    }
538                    _ if closing == b']' => return Err(self.error("expected `,` or `]`")),
539                    _ => return Err(self.error("expected `,` or `}`")),
540                }
541            }
542        }
543    }
544
545    /// Parse `"key" :`, leaving the parser on the value.
546    fn parse_key(&mut self) -> Result<String> {
547        self.skip_whitespace();
548        if self.peek() != Some(b'"') {
549            return Err(self.error("key must be a string"));
550        }
551        let key = self.parse_string()?;
552        self.skip_whitespace();
553        if self.peek() != Some(b':') {
554            return Err(self.error("expected `:`"));
555        }
556        self.pos += 1;
557        Ok(key)
558    }
559
560    fn parse_scalar(&mut self) -> Result<Node> {
561        match self.peek() {
562            Some(b'"') => Ok(Node::Str(self.parse_string()?)),
563            Some(b't') => {
564                self.expect_literal("true")?;
565                Ok(Node::Bool(true))
566            }
567            Some(b'f') => {
568                self.expect_literal("false")?;
569                Ok(Node::Bool(false))
570            }
571            Some(b'n') => {
572                self.expect_literal("null")?;
573                Ok(Node::Null)
574            }
575            // Python's json emits and accepts these three (`allow_nan=True` is
576            // the default both ways), so upstream renders documents containing
577            // them instead of rejecting the file.
578            Some(b'N') => {
579                self.expect_literal("NaN")?;
580                Ok(Node::NonFinite("NaN"))
581            }
582            Some(b'I') => {
583                self.expect_literal("Infinity")?;
584                Ok(Node::NonFinite("Infinity"))
585            }
586            Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
587                self.pos += "-Infinity".len();
588                Ok(Node::NonFinite("-Infinity"))
589            }
590            Some(b'-' | b'0'..=b'9') => self.parse_number(),
591            Some(_) => Err(self.error("expected value")),
592            None => Err(self.error("EOF while parsing a value")),
593        }
594    }
595
596    /// Read a string token and decode it with `serde_json`, so escapes, lone
597    /// surrogates and raw control characters behave exactly as before.
598    fn parse_string(&mut self) -> Result<String> {
599        let start = self.pos;
600        let mut end = self.pos + 1;
601        loop {
602            match self.bytes.get(end) {
603                None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
604                Some(b'"') => {
605                    end += 1;
606                    break;
607                }
608                Some(b'\\') => {
609                    end += 1;
610                    // Step over the escaped character whole. A multi-byte
611                    // character after a backslash is invalid JSON, but `end`
612                    // must still land on a UTF-8 boundary or slicing panics
613                    // before `serde_json` gets to reject it.
614                    match self.src[end..].chars().next() {
615                        Some(ch) => end += ch.len_utf8(),
616                        None => {
617                            return Err(
618                                self.error_at(self.bytes.len(), "EOF while parsing a string")
619                            )
620                        }
621                    }
622                }
623                // Continuation bytes are never `"` or `\`, so scanning byte by
624                // byte cannot mistake one for a delimiter.
625                Some(_) => end += 1,
626            }
627        }
628        let decoded: String = serde_json::from_str(&self.src[start..end])
629            .map_err(|error| self.error_at(start, &describe(&error)))?;
630        self.pos = end;
631        Ok(decoded)
632    }
633
634    /// Validate JSON's number grammar before decoding, preserving arbitrary-size
635    /// integers and Python's float overflow to Infinity (#74).
636    fn parse_number(&mut self) -> Result<Node> {
637        let start = self.pos;
638        let mut end = start;
639        while matches!(
640            self.bytes.get(end),
641            Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
642        ) {
643            end += 1;
644        }
645        let token = &self.src[start..end];
646        let digits = token.as_bytes();
647        let mut i = usize::from(digits.first() == Some(&b'-'));
648        if digits.get(i) == Some(&b'0') {
649            i += 1;
650        } else {
651            let first = i;
652            while digits.get(i).is_some_and(u8::is_ascii_digit) {
653                i += 1;
654            }
655            if i == first {
656                return Err(self.error_at(start, "invalid number"));
657            }
658        }
659        let mut floating = false;
660        if digits.get(i) == Some(&b'.') {
661            floating = true;
662            i += 1;
663            let first = i;
664            while digits.get(i).is_some_and(u8::is_ascii_digit) {
665                i += 1;
666            }
667            if i == first {
668                return Err(self.error_at(start, "invalid number"));
669            }
670        }
671        if matches!(digits.get(i), Some(b'e' | b'E')) {
672            floating = true;
673            i += 1;
674            if matches!(digits.get(i), Some(b'+' | b'-')) {
675                i += 1;
676            }
677            let first = i;
678            while digits.get(i).is_some_and(u8::is_ascii_digit) {
679                i += 1;
680            }
681            if i == first {
682                return Err(self.error_at(start, "invalid number"));
683            }
684        }
685        if i != digits.len() {
686            return Err(self.error_at(start, "invalid number"));
687        }
688        self.pos = end;
689        if !floating {
690            return Ok(Node::Number(
691                if token == "-0" { "0" } else { token }.to_string(),
692            ));
693        }
694        let value: f64 = token
695            .parse()
696            .map_err(|_| self.error_at(start, "invalid number"))?;
697        if value.is_infinite() {
698            return Ok(Node::NonFinite(if value.is_sign_negative() {
699                "-Infinity"
700            } else {
701                "Infinity"
702            }));
703        }
704        let number: serde_json::Number =
705            serde_json::from_str(token).map_err(|error| self.error_at(start, &describe(&error)))?;
706        Ok(Node::Number(number.to_string()))
707    }
708
709    fn expect_literal(&mut self, literal: &str) -> Result<()> {
710        if self.src[self.pos..].starts_with(literal) {
711            self.pos += literal.len();
712            Ok(())
713        } else {
714            Err(self.error("expected value"))
715        }
716    }
717
718    fn peek(&self) -> Option<u8> {
719        self.bytes.get(self.pos).copied()
720    }
721
722    fn skip_whitespace(&mut self) {
723        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
724            self.pos += 1;
725        }
726    }
727
728    fn error(&self, message: &str) -> RichError {
729        self.error_at(self.pos, message)
730    }
731
732    fn error_at(&self, pos: usize, message: &str) -> RichError {
733        let (line, column) = self.line_column(pos);
734        RichError::Json(format!("{message} at line {line} column {column}"))
735    }
736
737    fn line_column(&self, pos: usize) -> (usize, usize) {
738        let mut pos = pos.min(self.src.len());
739        while !self.src.is_char_boundary(pos) {
740            pos -= 1;
741        }
742        let before = &self.src[..pos];
743        let line = 1 + before.matches('\n').count();
744        let column = before
745            .rsplit('\n')
746            .next()
747            .map_or(0, |tail| tail.chars().count())
748            + 1;
749        (line, column)
750    }
751}
752
753/// `serde_json`'s message without its own `at line … column …` suffix, which
754/// counts from the start of the token slice rather than the document.
755fn describe(error: &serde_json::Error) -> String {
756    let text = error.to_string();
757    match text.find(" at line ") {
758        Some(at) => text[..at].to_string(),
759        None => text,
760    }
761}
762
763#[cfg(test)]
764mod tests {
765    use super::*;
766    use crate::color::ColorSystem;
767
768    fn render(text: &str) -> String {
769        let console = Console::builder()
770            .force_terminal(true)
771            .color_system(Some(ColorSystem::Truecolor))
772            .width(40)
773            .build();
774        console.render_to_string(&Json::new(text).unwrap())
775    }
776
777    fn render_plain(text: &str, width: usize) -> String {
778        let console = Console::builder().width(width).no_color(true).build();
779        console.render_to_string(&Json::new(text).expect("valid json"))
780    }
781
782    #[test]
783    fn empty_collections_stay_inline() {
784        assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
785        assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
786    }
787
788    #[test]
789    fn object_with_scalars() {
790        assert_eq!(
791            render(r#"{"ok": false}"#),
792            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
793        );
794    }
795
796    #[test]
797    fn invalid_json_errors() {
798        assert!(Json::new("{not json}").is_err());
799    }
800
801    #[test]
802    fn non_ascii_stays_utf8_in_input_order() {
803        // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
804        // characters render as UTF-8 (not \uXXXX), and keys keep input order.
805        // (Byte-parity is guaranteed by the `json_unicode` golden.)
806        let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
807        assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
808        assert!(out.contains('\u{2764}'), "heart stays UTF-8");
809        let name_at = out.find("name").expect("name key present");
810        let emoji_at = out.find("emoji").expect("emoji key present");
811        assert!(name_at < emoji_at, "keys keep input order");
812    }
813
814    #[test]
815    fn a_long_value_is_wrapped_rather_than_cropped() {
816        // A long string value used to be cut mid-token, so the printed document
817        // was missing data -- and, for JSON, no longer parseable -- at exit 0.
818        let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
819        let out = render_plain(&payload, 40);
820        assert_eq!(
821            out.matches('y').count(),
822            120,
823            "characters were dropped:
824{out}"
825        );
826    }
827
828    /// Upstream wraps the JSON at word boundaries: `JSON.text` asks for
829    /// `no_wrap`, but `Console.print` re-joins it through `Text(sep).join(...)`
830    /// and the joined copy carries neither the flag nor an overflow, so the
831    /// default `fold` word wrap applies. Character folding split words
832    /// (`over t` / `he lazy`), which no upstream output ever shows.
833    ///
834    /// Captured from rich 15.0.0:
835    /// `Console(width=40).print(JSON(...))`.
836    #[test]
837    fn wrapping_breaks_at_word_boundaries() {
838        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
839        assert_eq!(
840            render_plain(payload, 40),
841            "{\n  \"k\": \"the quick brown fox jumps over \n\
842             the lazy dog and keeps running for a \n\
843             very long time indeed\"\n}"
844        );
845    }
846
847    #[cfg(feature = "json-escape-safe")]
848    #[test]
849    fn adjusted_escape_boundaries_preserve_the_remaining_payload() {
850        let input = format!(r#"{{"v":"aa\u0001{}"}}"#, "b".repeat(40));
851        for width in 6..=20 {
852            let output = Console::builder()
853                .width(width)
854                .force_terminal(false)
855                .build()
856                .render_to_string(&Json::new(&input).unwrap().escape_safe(true));
857            assert_eq!(output.matches('b').count(), 40, "width {width}: {output:?}");
858        }
859    }
860
861    #[cfg(feature = "json-escape-safe")]
862    #[test]
863    fn wrapping_keeps_json_escapes_atomic_at_narrow_widths() {
864        let payload = r#"{"v":"a\"b\\c\nd\u0001e"}"#;
865        for width in 8..=14 {
866            let output = Console::builder()
867                .width(width)
868                .force_terminal(false)
869                .build()
870                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
871            for line in output.lines() {
872                let bytes = line.as_bytes();
873                let mut index = 0;
874                while index < bytes.len() {
875                    if bytes[index] != b'\\' {
876                        index += 1;
877                        continue;
878                    }
879                    assert!(
880                        index + 1 < bytes.len(),
881                        "split escape at width {width}: {output:?}"
882                    );
883                    if bytes[index + 1] == b'u' {
884                        assert!(
885                            index + 6 <= bytes.len(),
886                            "split unicode escape at width {width}: {output:?}"
887                        );
888                        index += 6;
889                    } else {
890                        index += 2;
891                    }
892                }
893            }
894        }
895    }
896
897    #[cfg(feature = "json-escape-safe")]
898    #[test]
899    fn escape_folding_preserves_bytes_even_below_the_escape_width() {
900        let payload = r#"{"v":"a\"b\\c\nd\u0001eeeeeeeeeeee"}"#;
901        let wide = Console::builder()
902            .width(100)
903            .force_terminal(false)
904            .build()
905            .render_to_string(&Json::new(payload).unwrap().escape_safe(true))
906            .replace('\n', "");
907        for width in 1..=20 {
908            let output = Console::builder()
909                .width(width)
910                .force_terminal(false)
911                .build()
912                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
913            assert_eq!(output.replace('\n', ""), wide, "width {width}");
914            assert!(output
915                .lines()
916                .all(|line| crate::cells::cell_len(line) <= width));
917        }
918    }
919
920    #[cfg(feature = "json-escape-safe")]
921    #[test]
922    fn escape_cropping_never_emits_a_partial_escape() {
923        let payload = r#""a\"b\\c\nd\u0001eeee""#;
924        for width in 1..=24 {
925            let output = Console::builder()
926                .width(width)
927                .force_terminal(false)
928                .build()
929                .render_to_string(&Json::new(payload).unwrap().no_wrap(true).escape_safe(true));
930            let mut chars = output.chars();
931            while let Some(c) = chars.next() {
932                if c == '\\' {
933                    let next = chars.next().expect("complete short escape");
934                    if next == 'u' {
935                        for _ in 0..4 {
936                            assert!(chars.next().is_some_and(|c| c.is_ascii_hexdigit()));
937                        }
938                    }
939                }
940            }
941        }
942    }
943
944    /// Nested inside another renderable, `JSON.text.no_wrap` survives and each
945    /// line is **cropped** at the width rather than wrapped — see
946    /// [`Json::no_wrap`]. Wrapping here instead was silent content loss: with
947    /// `rich -j doc.json -w 120` on an 80-column console the document was laid
948    /// out at 120 and then cropped to 80 by `Console.print`, so whole runs
949    /// vanished and the surviving text read as if it were contiguous.
950    ///
951    /// Captured from rich-cli 1.8.1 driven by rich 15.0.0:
952    /// `COLUMNS=80 rich -j long.json -w 40`.
953    #[test]
954    fn a_nested_document_is_cropped_rather_than_wrapped() {
955        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
956        let console = Console::builder().width(40).no_color(true).build();
957        let json = Json::new(payload).expect("valid json").no_wrap(true);
958        assert_eq!(
959            console.render_to_string(&json),
960            "{\n  \"k\": \"the quick brown fox jumps over t\n}"
961        );
962
963        // …and the wrap is still the default, because a bare
964        // `Console.print(JSON(...))` loses the flag in `Text.join`.
965        assert_eq!(
966            render_plain(payload, 40),
967            "{\n  \"k\": \"the quick brown fox jumps over \n\
968             the lazy dog and keeps running for a \n\
969             very long time indeed\"\n}"
970        );
971    }
972
973    /// A crop must not cut a double-width character in half: `set_cell_size`
974    /// drops the straddling character and the line comes out one cell short,
975    /// never one cell over.
976    #[test]
977    fn cropping_never_splits_a_wide_character() {
978        let console = Console::builder().width(12).no_color(true).build();
979        let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
980            .expect("valid json")
981            .no_wrap(true);
982        for line in console.render_to_string(&json).lines() {
983            assert!(
984                crate::cells::cell_len(line) <= 12,
985                "line {line:?} overflows the crop"
986            );
987        }
988    }
989
990    /// serde_json's default float parser takes a fast path that can land 1 ULP
991    /// from the value in the file, so the rendered number parsed back to a
992    /// *different* double. The `float_roundtrip` feature makes parsing exact.
993    #[test]
994    fn floats_round_trip_exactly() {
995        for literal in [
996            "-938371.9565467801",
997            "0.1",
998            "1.7976931348623157e308",
999            "5e-324",
1000            "3.141592653589793",
1001        ] {
1002            let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
1003            let rendered: String = out
1004                .split(':')
1005                .nth(1)
1006                .expect("a value after the key")
1007                .trim()
1008                .trim_end_matches(['}', ' ', '\n'])
1009                .to_string();
1010            let want: f64 = literal.parse().expect("literal parses");
1011            let got: f64 = rendered
1012                .parse()
1013                .unwrap_or_else(|_| panic!("rendered {rendered:?}"));
1014            assert_eq!(
1015                got.to_bits(),
1016                want.to_bits(),
1017                "{literal} rendered as {rendered} — a different double"
1018            );
1019        }
1020    }
1021
1022    /// serde_json stops at 128 levels, so a 200-deep document — which CPython
1023    /// parses without complaint — was reported as invalid JSON and the CLI
1024    /// exited 1 on a file upstream renders.
1025    #[test]
1026    fn deep_nesting_is_not_rejected() {
1027        for depth in [128, 129, 200, 1000] {
1028            let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
1029            let json = Json::new(&payload)
1030                .unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
1031            let console = Console::builder()
1032                .width(4 * depth + 8)
1033                .no_color(true)
1034                .build();
1035            let out = console.render_to_string(&json);
1036            assert_eq!(
1037                out.matches('[').count(),
1038                depth,
1039                "depth {depth} did not render every level"
1040            );
1041        }
1042    }
1043
1044    /// Parsing and dropping must cost heap, not stack: a recursive parser (or
1045    /// the compiler's recursive drop glue) turns a deep document into a stack
1046    /// overflow, which kills the process without even an error message.
1047    #[test]
1048    fn very_deep_nesting_does_not_overflow_the_stack() {
1049        let depth = 100_000;
1050        let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
1051        let json = Json::new(&payload).expect("deep document parses");
1052        drop(json);
1053    }
1054
1055    /// Python's json accepts and emits `NaN` / `Infinity` / `-Infinity`
1056    /// (`allow_nan=True` is the default), and rich's JSONHighlighter has no
1057    /// rule that matches them, so upstream prints them *unstyled*. serde_json
1058    /// rejected the whole document.
1059    ///
1060    /// Captured from rich 15.0.0:
1061    /// `Console(width=40, force_terminal=True).print(JSON(...))`.
1062    #[test]
1063    fn non_finite_numbers_render_unstyled() {
1064        assert_eq!(
1065            render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
1066            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"a\"\x1b[0m: NaN,\n  \x1b[1;34m\"b\"\x1b[0m: \
1067             Infinity,\n  \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n  \x1b[1;34m\"d\"\x1b[0m: \
1068             \x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
1069        );
1070        // Python spells them with those exact capitalisations and nothing else.
1071        for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
1072            assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
1073        }
1074    }
1075
1076    /// The hand-written reader must accept and reject exactly what serde_json
1077    /// does for bounded numbers — Python also accepts overflowing exponents.
1078    #[test]
1079    fn acceptance_matches_serde_json() {
1080        let samples = [
1081            "{}",
1082            "[]",
1083            "  {\t\"a\" :\n1 }  ",
1084            r#"{"a": 1, "a": 2}"#,
1085            r#"{"a": [1, {"b": null}], "c": "x"}"#,
1086            "0",
1087            "-0",
1088            "0.0",
1089            "1e10",
1090            "1E+10",
1091            "1e-7",
1092            "12345678901234567890",
1093            "-12345678901234567890123456789012345",
1094            "01",
1095            "1.",
1096            ".1",
1097            "+1",
1098            "1e",
1099            "-",
1100            "--1",
1101            "-i",
1102            "-Inf",
1103            "Infinit",
1104            "NAN",
1105            "1 2",
1106            "",
1107            "   ",
1108            "{",
1109            "[",
1110            "]",
1111            "}",
1112            "[,]",
1113            "[1,]",
1114            r#"{"a": 1,}"#,
1115            r#"{a: 1}"#,
1116            r#"{'a': 1}"#,
1117            r#"{"a" 1}"#,
1118            "[1 2]",
1119            "truex",
1120            "tru",
1121            "nul",
1122            r#""unterminated"#,
1123            r#""\q""#,
1124            r#""é""#,
1125            r#""😀""#,
1126            r#""\ud800""#,
1127            "\"raw\nnewline\"",
1128            r#""café ❤""#,
1129            "\"\u{e9}\\\"",
1130            "\u{feff}{}",
1131            "[[[[1]]]]",
1132        ];
1133        for sample in samples {
1134            let ours = Json::new(sample).is_ok();
1135            let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
1136            assert_eq!(ours, theirs, "disagreed about {sample:?}");
1137        }
1138    }
1139
1140    #[test]
1141    fn python_numbers_preserve_large_integers_and_overflow() {
1142        for number in [
1143            "1234567890123456789012345678901234567890",
1144            "-1234567890123456789012345678901234567890",
1145        ] {
1146            assert_eq!(render_plain(number, 100), number);
1147        }
1148        assert_eq!(render_plain("-0", 100), "0");
1149        assert_eq!(render_plain("1e400", 100), "Infinity");
1150        assert_eq!(render_plain("-1e999", 100), "-Infinity");
1151        for invalid in ["01", "-01", "1.e2", "1e+", "1e400x", "--1", "1+2", ".1"] {
1152            assert!(Json::new(invalid).is_err(), "accepted {invalid}");
1153        }
1154    }
1155
1156    /// And the tree it builds must be the tree serde_json would have built.
1157    /// `serde_json::to_string_pretty` happens to use the very layout upstream's
1158    /// `json.dumps(indent=2)` does, so it doubles as a reference dump: key
1159    /// order, repeated-key collapsing, string escaping and number formatting
1160    /// all have to agree.
1161    #[test]
1162    fn the_parsed_tree_matches_serde_json() {
1163        let samples = [
1164            r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
1165            r#"{"a": 1, "b": 2, "a": 3}"#,
1166            r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
1167            r#"{"k": "A\t\"x\"A\\\/é"}"#,
1168            r#"[0, -0.5, 1e10, 1E+10, 1e-7, 12345678901234567890, 1.7976931348623157e308]"#,
1169            r#"{"café": "❤", "": ""}"#,
1170            "[]",
1171            "{}",
1172            "\"top level\"",
1173            "1234",
1174        ];
1175        for sample in samples {
1176            let reference: serde_json::Value =
1177                serde_json::from_str(sample).expect("sample is valid JSON");
1178            assert_eq!(
1179                render_plain(sample, 10_000),
1180                serde_json::to_string_pretty(&reference).expect("value re-serialises"),
1181                "diverged on {sample}"
1182            );
1183        }
1184    }
1185
1186    /// A repeated key collapses to one entry — first position, last value —
1187    /// which is what both `dict` and serde_json's `preserve_order` produce.
1188    #[test]
1189    fn a_repeated_key_keeps_its_position_and_last_value() {
1190        assert_eq!(
1191            render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
1192            "{\n  \"a\": 3,\n  \"b\": 2\n}"
1193        );
1194    }
1195
1196    /// Escapes are decoded and re-encoded, because upstream re-serialises the
1197    /// parsed data with `json.dumps`.
1198    #[test]
1199    fn escapes_are_re_encoded_like_dumps() {
1200        assert_eq!(
1201            render_plain(r#"{"k": "A\t\"x\""}"#, 60),
1202            "{\n  \"k\": \"A\\t\\\"x\\\"\"\n}"
1203        );
1204    }
1205}