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. Number formatting follows
11//! `json.dumps` too: floats are written
12//! with Python's `float.__repr__` ([`crate::pyformat::float_repr`]).
13//! [`JsonOptions`] carries upstream's `indent`, `sort_keys`, `ensure_ascii`,
14//! `allow_nan` and `highlight`; highlighting is upstream's `JSONHighlighter`.
15//!
16//! ## Why the parser is hand-written
17//!
18//! Upstream's parser is Python's `json`, which differs from `serde_json` in two
19//! important ways this module reproduces:
20//!
21//! * `json.loads` accepts (and `json.dumps(allow_nan=True)` emits) the
22//!   non-finite literals `NaN`, `Infinity` and `-Infinity`. `serde_json` has no
23//!   `Value` that can hold them and rejects the documents outright.
24//! * `serde_json` caps nesting at 128 levels, so a 200-deep document — which
25//!   CPython parses without complaint — came back as "invalid JSON".
26//!
27//! Raising a recursion limit only moves the failure to a stack overflow, so
28//! parsing, rendering and *dropping* the tree here are all iterative: nesting
29//! depth costs heap, never stack. String decoding uses `serde_json`; integer
30//! tokens retain their exact digits, finite floats use Python's `repr`, and
31//! overflowing exponents become signed Infinity as in Python.
32//!
33//! Nesting is still bounded, at [`MAX_DEPTH`] levels, where CPython raises
34//! `RecursionError`: `json.loads` recurses in C, and CPython 3.12+ stops just
35//! short of 10 000 levels (3.11 at its 1 000-frame recursion limit). Rendering
36//! indents every line by its depth, so an unbounded document costs quadratic
37//! memory: a 100 000-deep array asked for tens of gigabytes and was killed
38//! rather than reporting an error. The limit accepts every document CPython
39//! 3.12+ accepts; the few levels between its exact (stack-dependent) cutoff
40//! and `MAX_DEPTH` are the only difference.
41
42use std::collections::HashMap;
43
44/// The deepest nesting [`Json::new`] accepts (DIVERGENCES #25). Deeper
45/// documents fail with the error CPython's `json.loads` raises
46/// (`RecursionError`); see the module docs.
47pub const MAX_DEPTH: usize = 10_000;
48
49use crate::console::{Console, ConsoleOptions};
50use crate::errors::{Result, RichError};
51use crate::measure::Measurement;
52use crate::protocol::Renderable;
53use crate::segment::Segment;
54use crate::style::StyleType;
55use crate::text::{Span, Text};
56
57/// A parsed JSON document, rendered with syntax highlighting. Mirrors `rich.json.JSON`.
58///
59/// Like upstream, the document is re-serialized (`json.dumps`) with the
60/// [`JsonOptions`] and highlighted with `JSONHighlighter` when it is built;
61/// [`text`](Json::text) is that highlighted `Text`.
62pub struct Json {
63    value: Node,
64    options: JsonOptions,
65    /// The highlighted document, one `Text` per physical line (upstream
66    /// wraps a `Text` line by line, so this is how it renders). Built on
67    /// first use: a document near [`MAX_DEPTH`] formats to hundreds of
68    /// megabytes of indentation, and one that is only parsed and dropped
69    /// should not pay for that.
70    lines: std::sync::OnceLock<Vec<Text>>,
71    /// See [`Json::no_wrap`].
72    no_wrap: bool,
73    #[cfg(feature = "json-escape-safe")]
74    escape_safe: bool,
75}
76
77/// The keyword arguments of `rich.json.JSON`, for [`Json::with_options`].
78///
79/// `skip_keys` and `check_circular` are accepted for parity but cannot change
80/// the output of a document parsed from a string: its keys are all strings
81/// and it has no cycles. Upstream's `default=` callable only applies to
82/// `JSON.from_data`, which serializes arbitrary (non-JSON) objects.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct JsonOptions {
85    /// `indent=`: the indent string, or `None` for a single line. An integer
86    /// indent `n` is `n` spaces (see [`JsonOptions::indent_spaces`]).
87    pub indent: Option<String>,
88    /// `highlight=`: apply `JSONHighlighter` (default on).
89    pub highlight: bool,
90    /// `skip_keys=` (no effect on a parsed document).
91    pub skip_keys: bool,
92    /// `ensure_ascii=`: escape every non-ASCII character (default off).
93    pub ensure_ascii: bool,
94    /// `check_circular=` (no effect on a parsed document).
95    pub check_circular: bool,
96    /// `allow_nan=`: with it off, `NaN` and the infinities are an error
97    /// (default on).
98    pub allow_nan: bool,
99    /// `sort_keys=`: sort object keys (default off).
100    pub sort_keys: bool,
101}
102
103impl Default for JsonOptions {
104    fn default() -> Self {
105        JsonOptions {
106            indent: Some("  ".to_string()),
107            highlight: true,
108            skip_keys: false,
109            ensure_ascii: false,
110            check_circular: true,
111            allow_nan: true,
112            sort_keys: false,
113        }
114    }
115}
116
117impl JsonOptions {
118    /// An integer `indent=`: `n` spaces, as `json.dumps` turns an int into
119    /// `" " * n` (so zero or a negative number puts each item on its own line
120    /// with no indentation).
121    pub fn indent_spaces(indent: i64) -> Option<String> {
122        Some(" ".repeat(usize::try_from(indent).unwrap_or(0)))
123    }
124}
125
126/// A Python `str` as generalized UTF-8 (WTF-8): UTF-8, except that a lone
127/// surrogate from a `\uD800`-style escape (which Python's `json` accepts)
128/// is kept as its three-byte encoding, as `surrogatepass` writes it. Byte
129/// order is still code point order, so `sort_keys` sorts as Python does.
130type PyStr = Vec<u8>;
131
132/// A parsed JSON value.
133///
134/// Scalars keep the form they will be printed in: numbers are stored already
135/// normalized where needed, strings already decoded (upstream re-encodes them
136/// through `json.dumps`, so `"A"` prints as `"A"`).
137#[derive(Debug)]
138enum Node {
139    Null,
140    Bool(bool),
141    Number(String),
142    /// `NaN`, `Infinity` or `-Infinity`. Python's `json` round-trips these;
143    /// rich's `JSONHighlighter` has no rule that matches them, so they print
144    /// unstyled.
145    NonFinite(&'static str),
146    Str(PyStr),
147    Array(Vec<Node>),
148    Object(Vec<(PyStr, Node)>),
149}
150
151impl Drop for Node {
152    /// Dismantle the tree with an explicit stack.
153    ///
154    /// The compiler's drop glue recurses once per nesting level, so a document
155    /// deep enough to parse (parsing has no depth limit here) would overflow
156    /// the stack on the way out — a crash with no error message at all, which
157    /// is worse than the rejection this module used to hand out.
158    fn drop(&mut self) {
159        let mut pending: Vec<Node> = Vec::new();
160        take_children(self, &mut pending);
161        while let Some(mut node) = pending.pop() {
162            take_children(&mut node, &mut pending);
163            // `node` drops here with its children already moved out, so this
164            // same `drop` runs against an empty container and stops.
165        }
166    }
167}
168
169/// Move a node's children into `out`, leaving the node childless.
170fn take_children(node: &mut Node, out: &mut Vec<Node>) {
171    match node {
172        Node::Array(items) => out.append(items),
173        Node::Object(entries) => out.extend(entries.drain(..).map(|(_, value)| value)),
174        _ => {}
175    }
176}
177
178impl Json {
179    /// Parse `text` as JSON with upstream's default options. Returns an error
180    /// if it is not valid JSON.
181    pub fn new(text: &str) -> Result<Self> {
182        Json::with_options(text, &JsonOptions::default())
183    }
184
185    /// Format data as JSON with `options`. Port of `JSON.from_data` for the
186    /// data JSON can hold (Rust has no arbitrary objects to `default=`):
187    /// objects keep their key order, as a Python `dict` does.
188    pub fn from_data(data: &serde_json::Value, options: &JsonOptions) -> Result<Self> {
189        let text = serde_json::to_string(data).map_err(|e| RichError::Json(e.to_string()))?;
190        Json::with_options(&text, options)
191    }
192
193    /// Parse `text` as JSON and format it with `options`. Port of
194    /// `rich.json.JSON(json, indent=…, highlight=…, …)`.
195    pub fn with_options(text: &str, options: &JsonOptions) -> Result<Self> {
196        let value = Parser::new(text).parse_document()?;
197        // `json.dumps(allow_nan=False)` raises here, at construction.
198        if !options.allow_nan
199            && let Some(literal) = first_non_finite(&value)
200        {
201            return Err(RichError::Json(format!(
202                "Out of range float values are not JSON compliant: {}",
203                match literal {
204                    "NaN" => "nan",
205                    "Infinity" => "inf",
206                    _ => "-inf",
207                }
208            )));
209        }
210        Ok(Json {
211            value,
212            options: options.clone(),
213            lines: std::sync::OnceLock::new(),
214            no_wrap: false,
215            #[cfg(feature = "json-escape-safe")]
216            escape_safe: false,
217        })
218    }
219
220    /// The formatted document, one highlighted `Text` per line.
221    fn lines(&self) -> &[Text] {
222        self.lines.get_or_init(|| {
223            let dumped = dumps(&self.value, &self.options);
224            let text = if !self.options.highlight {
225                Text::new(dumped.json)
226            } else if dumped.exact {
227                let mut text = Text::new(dumped.json);
228                let mut spans = dumped.spans;
229                spans.extend(dumped.keys);
230                text.set_spans(spans);
231                text
232            } else {
233                json_highlight(dumped.json)
234            };
235            if text.plain().contains('\n') {
236                text.split("\n", false, true)
237            } else {
238                vec![text]
239            }
240        })
241    }
242
243    /// The formatted, highlighted document. Port of the `JSON.text`
244    /// attribute (upstream's `__rich__` returns it).
245    pub fn text(&self) -> Text {
246        match self.lines() {
247            [line] => line.clone(),
248            lines => Text::new("\n").join(lines),
249        }
250    }
251
252    /// Opt in to escape-aware display boundaries (requires `json-escape-safe`).
253    /// Cropping omits partial escapes. Folding preserves escapes that fit the
254    /// width; narrower widths split oversized escapes to avoid losing content.
255    /// The default remains Python rich's ordinary word folding/cropping.
256    #[cfg(feature = "json-escape-safe")]
257    pub fn escape_safe(mut self, enabled: bool) -> Self {
258        self.escape_safe = enabled;
259        self
260    }
261
262    /// Keep `rich.json.JSON`'s `self.text.no_wrap = True`, which **crops** each
263    /// line at the available width instead of wrapping it.
264    ///
265    /// Defaults to `false`, because that is what a *top-level*
266    /// `Console.print(JSON(...))` produces and that is how this renderable is
267    /// normally reached. Upstream's flag really is set, but `Console.print`
268    /// never renders the `Text` it is set on: `_collect_renderables` sees a
269    /// `Text`, buffers it, and `check_text` hands back
270    /// `Text(sep, justify=…, end=…).join(buffered)` — and `Text.join` starts
271    /// from `self.blank_copy()`, i.e. from the *separator's* metadata. The
272    /// separator has `no_wrap=None`, so the copy that actually renders wraps
273    /// with the default `fold` overflow — or with the `no_wrap` and
274    /// `overflow` that `print` was given, which reach it as
275    /// [`ConsoleOptions`] and which this renderable honours while the flag is
276    /// off.
277    ///
278    /// Turn it on whenever the document is **nested** inside another
279    /// renderable — a `Panel`, `Padding`, `Styled`, `Constrain`, or rich-cli's
280    /// `ForceWidth`. Those are `ConsoleRenderable`s, so `_collect_renderables`
281    /// appends them untouched and the inner `Text` reaches
282    /// `Text.__rich_console__` with the flag intact; `Text.wrap` then skips
283    /// `divide_line` and only `truncate`s to the width.
284    ///
285    /// The two are not interchangeable. Wrapping keeps every character;
286    /// cropping discards what does not fit, which for JSON means the printed
287    /// document no longer parses — so the choice has to follow upstream's,
288    /// not taste.
289    #[must_use]
290    pub fn no_wrap(mut self, no_wrap: bool) -> Self {
291        self.no_wrap = no_wrap;
292        self
293    }
294
295    /// The document's lines rendered without wrapping, joined by newlines.
296    #[cfg(feature = "json-escape-safe")]
297    fn render_unwrapped(&self, console: &Console) -> Vec<Segment> {
298        let mut out = Vec::new();
299        for (index, line) in self.lines().iter().enumerate() {
300            if index > 0 {
301                out.push(Segment::line());
302            }
303            out.extend(line.render(console.theme(), console.base_style()));
304        }
305        out
306    }
307}
308
309/// A serialized document, with the spans `JSONHighlighter` would give it.
310struct Dumped {
311    json: String,
312    /// Token spans (`json.brace`, `json.str`, …) in document order.
313    spans: Vec<Span>,
314    /// `json.key` spans, which upstream appends after every token span.
315    keys: Vec<Span>,
316    /// Whether `spans` + `keys` are exactly what the highlighter's regexes
317    /// produce. They are for a whitespace indent and strings whose closing
318    /// quote does not follow a backslash; otherwise the regexes run.
319    exact: bool,
320}
321
322impl Dumped {
323    fn styled(&mut self, token: &str, style: &'static str) {
324        let start = self.json.len();
325        self.json.push_str(token);
326        self.spans.push(Span {
327            start,
328            end: self.json.len(),
329            style: StyleType::Name(style.to_string()),
330        });
331    }
332
333    /// A quoted string. `JSON_STR`'s lazy `".*?(?<!\\)"` ends at the first
334    /// quote not preceded by a backslash, which is this string's own closing
335    /// quote unless the string ends in an escaped backslash.
336    fn string(&mut self, quoted: &str, key: bool) {
337        if quoted.as_bytes().get(quoted.len().wrapping_sub(2)) == Some(&b'\\') {
338            self.exact = false;
339        }
340        let start = self.json.len();
341        self.styled(quoted, "json.str");
342        if key {
343            self.keys.push(Span {
344                start,
345                end: self.json.len(),
346                style: StyleType::Name("json.key".to_string()),
347            });
348        }
349    }
350}
351
352/// Serialize `value` as Python's `json.dumps` does with `options`, iteratively
353/// so nesting depth costs heap rather than stack.
354fn dumps(value: &Node, options: &JsonOptions) -> Dumped {
355    use std::borrow::Cow;
356    let indent = options.indent.as_deref();
357    // `json.dumps` separators: `(', ', ': ')` on one line, `(',', ': ')` with
358    // an indent.
359    let item_separator = if indent.is_some() { "," } else { ", " };
360    let newline_indent = |level: usize| -> Cow<'static, str> {
361        match indent {
362            Some(indent) => Cow::Owned(format!("\n{}", indent.repeat(level))),
363            None => Cow::Borrowed(""),
364        }
365    };
366    let quote_string = |string: &[u8]| -> String { quote_py(string, options.ensure_ascii) };
367
368    /// One entry of the serializer's work list, consumed newest-first.
369    enum Task<'a> {
370        /// Serialize this value, `usize` levels deep.
371        Value(&'a Node, usize),
372        /// Write this unstyled text.
373        Plain(Cow<'a, str>),
374        /// Write a closing brace.
375        Close(&'static str),
376        /// Write an object key and its `": "`.
377        Key(&'a [u8]),
378    }
379
380    let mut dumped = Dumped {
381        json: String::new(),
382        spans: Vec::new(),
383        keys: Vec::new(),
384        // The highlighter's patterns could match inside any other indent.
385        exact: indent.is_none_or(|indent| indent.chars().all(|c| c == ' ' || c == '\t')),
386    };
387    let mut stack = vec![Task::Value(value, 0)];
388    while let Some(task) = stack.pop() {
389        let (node, level) = match task {
390            Task::Plain(text) => {
391                dumped.json.push_str(&text);
392                continue;
393            }
394            Task::Close(brace) => {
395                dumped.styled(brace, "json.brace");
396                continue;
397            }
398            Task::Key(key) => {
399                dumped.string(&quote_string(key), true);
400                dumped.json.push_str(": ");
401                continue;
402            }
403            Task::Value(node, level) => (node, level),
404        };
405        match node {
406            Node::Null => dumped.styled("null", "json.null"),
407            Node::Bool(true) => dumped.styled("true", "json.bool_true"),
408            Node::Bool(false) => dumped.styled("false", "json.bool_false"),
409            Node::Number(number) => dumped.styled(number, "json.number"),
410            // No `JSONHighlighter` pattern matches `NaN` or the infinities.
411            Node::NonFinite(literal) => dumped.json.push_str(literal),
412            Node::Str(string) => dumped.string(&quote_string(string), false),
413            Node::Array(items) => {
414                dumped.styled("[", "json.brace");
415                if items.is_empty() {
416                    dumped.styled("]", "json.brace");
417                    continue;
418                }
419                // Pushed back-to-front, so they pop in document order.
420                stack.push(Task::Close("]"));
421                stack.push(Task::Plain(newline_indent(level)));
422                let last = items.len() - 1;
423                for (index, item) in items.iter().enumerate().rev() {
424                    if index != last {
425                        stack.push(Task::Plain(Cow::Borrowed(item_separator)));
426                    }
427                    stack.push(Task::Value(item, level + 1));
428                    stack.push(Task::Plain(newline_indent(level + 1)));
429                }
430            }
431            Node::Object(entries) => {
432                dumped.styled("{", "json.brace");
433                if entries.is_empty() {
434                    dumped.styled("}", "json.brace");
435                    continue;
436                }
437                let mut order: Vec<&(PyStr, Node)> = entries.iter().collect();
438                if options.sort_keys {
439                    // `sorted(dct.items())`: keys are unique, so this is by key,
440                    // in code point order (which UTF-8 byte order preserves).
441                    order.sort_by(|a, b| a.0.cmp(&b.0));
442                }
443                stack.push(Task::Close("}"));
444                stack.push(Task::Plain(newline_indent(level)));
445                let last = order.len() - 1;
446                for (index, (key, item)) in order.into_iter().enumerate().rev() {
447                    if index != last {
448                        stack.push(Task::Plain(Cow::Borrowed(item_separator)));
449                    }
450                    stack.push(Task::Value(item, level + 1));
451                    stack.push(Task::Key(key));
452                    stack.push(Task::Plain(newline_indent(level + 1)));
453                }
454            }
455        }
456    }
457    dumped
458}
459
460/// The first non-finite literal in the document, if any, found iteratively.
461fn first_non_finite(value: &Node) -> Option<&'static str> {
462    let mut pending = vec![value];
463    while let Some(node) = pending.pop() {
464        match node {
465            Node::NonFinite(literal) => return Some(literal),
466            Node::Array(items) => pending.extend(items.iter()),
467            Node::Object(entries) => pending.extend(entries.iter().map(|(_, value)| value)),
468            _ => {}
469        }
470    }
471    None
472}
473
474/// `ensure_ascii=True`: re-escape every character outside `' '..='~'` in an
475/// already-quoted JSON string as `\uXXXX` (a surrogate pair above the BMP),
476/// as `py_encode_basestring_ascii` does. The quoted form's own escapes are
477/// ASCII already.
478fn ascii_escape(quoted: &str) -> String {
479    if quoted.bytes().all(|b| (b' '..=b'~').contains(&b)) {
480        return quoted.to_string();
481    }
482    let mut out = String::with_capacity(quoted.len() + 16);
483    for c in quoted.chars() {
484        if (' '..='~').contains(&c) {
485            out.push(c);
486            continue;
487        }
488        let mut units = [0u16; 2];
489        for unit in c.encode_utf16(&mut units) {
490            out.push_str(&format!("\\u{unit:04x}"));
491        }
492    }
493    out
494}
495
496/// Upstream's `JSONHighlighter` regexes: the combined token pattern, and the
497/// string pattern its key pass re-scans with.
498fn json_patterns() -> &'static (fancy_regex::Regex, fancy_regex::Regex) {
499    static PATTERNS: std::sync::OnceLock<(fancy_regex::Regex, fancy_regex::Regex)> =
500        std::sync::OnceLock::new();
501    PATTERNS.get_or_init(|| {
502        const JSON_STR: &str = r#"(?<![\\\w])(?P<str>b?".*?(?<!\\)")"#;
503        let combined = [
504            r"(?P<brace>[\{\[\(\)\]\}])",
505            r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b",
506            r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
507            JSON_STR,
508        ]
509        .join("|");
510        (
511            fancy_regex::Regex::new(&combined).expect("valid JSON highlighter pattern"),
512            fancy_regex::Regex::new(JSON_STR).expect("valid JSON string pattern"),
513        )
514    })
515}
516
517/// Highlight a formatted document. Port of `JSONHighlighter.highlight`: the
518/// `json.`-prefixed token spans, then a `json.key` span on every string that
519/// is followed (past whitespace) by a colon.
520fn json_highlight(json: String) -> Text {
521    let (combined, string) = json_patterns();
522    let mut text = Text::new(json);
523    text.highlight_with_regex(combined, None, "json.");
524    let plain = text.plain();
525    let mut keys = Vec::new();
526    for found in string.find_iter(plain) {
527        let Ok(found) = found else { break };
528        for &byte in &plain.as_bytes()[found.end()..] {
529            match byte {
530                b':' => keys.push((found.start(), found.end())),
531                b' ' | b'\n' | b'\r' | b'\t' => continue,
532                _ => {}
533            }
534            break;
535        }
536    }
537    for (start, end) in keys {
538        text.push_span(Span {
539            start,
540            end,
541            style: StyleType::Name("json.key".to_string()),
542        });
543    }
544    text
545}
546
547impl Renderable for Json {
548    /// Upstream `JSON.__rich__` returns its highlighted `Text`, so measuring a
549    /// `JSON` is `Text.__rich_measure__` over the formatted document.
550    fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> Measurement {
551        let mut minimum = 0;
552        let mut maximum = 0;
553        for line in self.lines() {
554            let (line_minimum, line_maximum) = line.measurement();
555            minimum = minimum.max(line_minimum);
556            maximum = maximum.max(line_maximum);
557        }
558        Measurement::new(minimum, maximum)
559    }
560
561    /// `Text.__rich_console__` of the document: each line wraps (or, with
562    /// `no_wrap`, is truncated) to the width with the options' `justify` and
563    /// `overflow` — the document's own `overflow` is `None`.
564    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
565        #[cfg(feature = "json-escape-safe")]
566        if self.escape_safe {
567            let segments = self.render_unwrapped(console);
568            return escape_safe_lines(&segments, options.max_width, self.no_wrap);
569        }
570        let no_wrap = self.no_wrap || options.no_wrap.unwrap_or(false);
571        let overflow = options.overflow.unwrap_or_default();
572        let tab_size = match console.tab_size() {
573            0 => crate::text::DEFAULT_TAB_SIZE,
574            tab_size => tab_size,
575        };
576        let mut out = Vec::new();
577        let mut first = true;
578        for line in self.lines() {
579            for rendered in line.render_lines_wrapped_tabs(
580                console.theme(),
581                console.base_style(),
582                Some(options.max_width),
583                options.justify,
584                overflow,
585                no_wrap,
586                tab_size,
587            ) {
588                if !first {
589                    out.push(Segment::line());
590                }
591                first = false;
592                out.extend(rendered);
593            }
594        }
595        out
596    }
597}
598
599/// Tokenize each physical line into JSON escapes and ordinary graphemes, then
600/// choose every boundary from the space actually remaining. No stale absolute
601/// wrap points survive an adjusted escape boundary (#98).
602#[cfg(feature = "json-escape-safe")]
603fn escape_safe_lines(segments: &[Segment], width: usize, crop: bool) -> Vec<Segment> {
604    if width == 0 {
605        return Vec::new();
606    }
607    let lines = Segment::split_lines(segments);
608    let last = lines.len().saturating_sub(1);
609    let mut out = Vec::new();
610    for (line_index, line) in lines.into_iter().enumerate() {
611        let plain: String = line
612            .iter()
613            .filter(|s| !s.control)
614            .map(|s| s.text.as_str())
615            .collect();
616        // Keep upstream's exact behavior when there is no escape to protect.
617        if !plain.contains('\\') {
618            out.extend(if crop {
619                Segment::crop_lines(&line, width)
620            } else {
621                Segment::fold_lines_words(&line, width)
622            });
623        } else {
624            let (spans, _) = crate::cells::split_graphemes(&plain);
625            let mut atoms = Vec::new();
626            let mut index = 0;
627            while index < spans.len() {
628                let (start, mut end, mut cells) = spans[index];
629                if plain.as_bytes()[start] == b'\\' {
630                    let escape_end = start
631                        + if plain.as_bytes().get(start + 1) == Some(&b'u') {
632                            6
633                        } else {
634                            2
635                        };
636                    while end < escape_end && index + 1 < spans.len() {
637                        index += 1;
638                        end = spans[index].1;
639                        cells += spans[index].2;
640                    }
641                    if !crop && cells > width {
642                        // An atom wider than the whole console cannot both fit
643                        // and stay atomic. Split its ASCII spelling rather than
644                        // overrun into Console's final crop and lose characters.
645                        for offset in start..escape_end {
646                            atoms.push((offset, offset + 1, 1));
647                        }
648                        if end > escape_end {
649                            atoms.push((escape_end, end, 0));
650                        }
651                        index += 1;
652                        continue;
653                    }
654                }
655                atoms.push((start, end, cells));
656                index += 1;
657            }
658            let mut breaks = Vec::new();
659            let mut cells = 0;
660            let mut stop = plain.len();
661            for (start, _, atom_width) in atoms {
662                if cells + atom_width > width {
663                    if crop {
664                        stop = start;
665                        break;
666                    }
667                    if cells > 0 {
668                        breaks.push(start);
669                        cells = 0;
670                    }
671                }
672                cells += atom_width;
673            }
674            let mut position = 0;
675            let mut next = 0;
676            for segment in line {
677                if segment.control {
678                    out.push(segment);
679                    continue;
680                }
681                let mut buffer = String::new();
682                for ch in segment.text.chars() {
683                    if position >= stop {
684                        break;
685                    }
686                    if breaks.get(next) == Some(&position) {
687                        if !buffer.is_empty() {
688                            out.push(Segment::new(
689                                std::mem::take(&mut buffer),
690                                segment.style.clone(),
691                            ));
692                        }
693                        out.push(Segment::line());
694                        next += 1;
695                    }
696                    buffer.push(ch);
697                    position += ch.len_utf8();
698                }
699                if !buffer.is_empty() {
700                    out.push(Segment::new(buffer, segment.style));
701                }
702            }
703        }
704        if line_index != last {
705            out.push(Segment::line());
706        }
707    }
708    out
709}
710
711/// Serialize a string as a JSON string literal (quoted + escaped).
712/// `json.dumps` of a [`PyStr`]. A lone surrogate is `\udXXX` under
713/// `ensure_ascii`; otherwise Python emits the surrogate itself, which a Rust
714/// string cannot hold, so it becomes U+FFFD (see docs/DIVERGENCES.md).
715fn quote_py(string: &[u8], ensure_ascii: bool) -> String {
716    let escape = |run: &str| -> String {
717        let quoted = quote(run);
718        if ensure_ascii {
719            ascii_escape(&quoted)
720        } else {
721            quoted
722        }
723    };
724    if let Ok(run) = std::str::from_utf8(string) {
725        return escape(run);
726    }
727    let mut out = String::from("\"");
728    let mut rest = string;
729    while !rest.is_empty() {
730        let valid = match std::str::from_utf8(rest) {
731            Ok(_) => rest.len(),
732            Err(error) => error.valid_up_to(),
733        };
734        let (run, tail) = rest.split_at(valid);
735        let quoted = escape(std::str::from_utf8(run).unwrap_or_default());
736        out.push_str(&quoted[1..quoted.len() - 1]);
737        rest = tail;
738        if let [0xED, b1 @ 0xA0..=0xBF, b2, tail @ ..] = rest {
739            let unit = 0xD000 | (u32::from(b1 & 0x3F) << 6) | u32::from(b2 & 0x3F);
740            if ensure_ascii {
741                out.push_str(&format!("\\u{unit:04x}"));
742            } else {
743                out.push(char::REPLACEMENT_CHARACTER);
744            }
745            rest = tail;
746        } else if !rest.is_empty() {
747            // Unreachable for parser output; never loop forever.
748            out.push(char::REPLACEMENT_CHARACTER);
749            rest = &rest[1..];
750        }
751    }
752    out.push('"');
753    out
754}
755
756/// Decode a JSON string token as Python's `json` does, keeping unpaired
757/// `\uD800`-`\uDFFF` escapes as surrogates in the result. `None` when the
758/// token is invalid for another reason (the caller reports `serde_json`'s
759/// error for it).
760fn decode_with_surrogates(token: &str) -> Option<PyStr> {
761    let inner = token.strip_prefix('"')?.strip_suffix('"')?;
762    let mut out = Vec::with_capacity(inner.len());
763    let mut chars = inner.chars().peekable();
764    let hex4 = |chars: &mut std::iter::Peekable<std::str::Chars<'_>>| -> Option<u32> {
765        let mut value = 0;
766        for _ in 0..4 {
767            value = value * 16 + chars.next()?.to_digit(16)?;
768        }
769        Some(value)
770    };
771    while let Some(c) = chars.next() {
772        if c < ' ' {
773            return None;
774        }
775        if c != '\\' {
776            let mut buf = [0u8; 4];
777            out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
778            continue;
779        }
780        let decoded = match chars.next()? {
781            '"' => '"',
782            '\\' => '\\',
783            '/' => '/',
784            'b' => '\u{8}',
785            'f' => '\u{c}',
786            'n' => '\n',
787            'r' => '\r',
788            't' => '\t',
789            'u' => {
790                let mut unit = hex4(&mut chars)?;
791                if (0xD800..0xDC00).contains(&unit) {
792                    let mut lookahead = chars.clone();
793                    if lookahead.next() == Some('\\')
794                        && lookahead.next() == Some('u')
795                        && let Some(low @ 0xDC00..=0xDFFF) = hex4(&mut lookahead)
796                    {
797                        unit = 0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00);
798                        chars = lookahead;
799                    }
800                }
801                match char::from_u32(unit) {
802                    Some(c) => c,
803                    None => {
804                        // A lone surrogate: its generalized UTF-8 encoding.
805                        out.push(0xE0 | (unit >> 12) as u8);
806                        out.push(0x80 | ((unit >> 6) & 0x3F) as u8);
807                        out.push(0x80 | (unit & 0x3F) as u8);
808                        continue;
809                    }
810                }
811            }
812            _ => return None,
813        };
814        let mut buf = [0u8; 4];
815        out.extend_from_slice(decoded.encode_utf8(&mut buf).as_bytes());
816    }
817    Some(out)
818}
819
820fn quote(string: &str) -> String {
821    serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
822}
823
824/// A container being filled in, held on the parser's explicit stack.
825enum Frame {
826    Array(Vec<Node>),
827    Object {
828        entries: Vec<(PyStr, Node)>,
829        /// Key -> position in `entries`, so a repeated key overwrites in place
830        /// (`{"a": 1, "a": 2}` is one entry) without an O(n^2) rescan. Dropped
831        /// with the frame, so only the objects on the current path pay for it.
832        seen: HashMap<PyStr, usize>,
833        /// The key whose value is currently being parsed.
834        key: PyStr,
835    },
836}
837
838/// A non-recursive JSON reader. Structure is walked with an explicit stack;
839/// scalar tokens are handed to `serde_json` for decoding so escapes, number
840/// formats and their rejections stay identical to the rest of the workspace.
841struct Parser<'a> {
842    src: &'a str,
843    bytes: &'a [u8],
844    pos: usize,
845}
846
847impl<'a> Parser<'a> {
848    fn new(src: &'a str) -> Self {
849        Parser {
850            src,
851            bytes: src.as_bytes(),
852            pos: 0,
853        }
854    }
855
856    fn parse_document(&mut self) -> Result<Node> {
857        let value = self.parse_value()?;
858        self.skip_whitespace();
859        if self.pos != self.bytes.len() {
860            return Err(self.error("trailing characters"));
861        }
862        Ok(value)
863    }
864
865    /// Parse one value, descending into containers with an explicit stack.
866    fn parse_value(&mut self) -> Result<Node> {
867        let mut stack: Vec<Frame> = Vec::new();
868        let mut node: Node;
869
870        'descend: loop {
871            self.skip_whitespace();
872            // CPython's scanner enters a recursive call per container, empty
873            // ones included, and raises once the recursion limit is reached.
874            if let Some(kind @ (b'[' | b'{')) = self.peek()
875                && stack.len() >= MAX_DEPTH
876            {
877                let what = if kind == b'[' { "array" } else { "object" };
878                return Err(self.error(&format!(
879                    "maximum recursion depth exceeded while decoding a JSON {what} \
880                         from a unicode string"
881                )));
882            }
883            match self.peek() {
884                Some(b'[') => {
885                    self.pos += 1;
886                    self.skip_whitespace();
887                    if self.peek() == Some(b']') {
888                        self.pos += 1;
889                        node = Node::Array(Vec::new());
890                    } else {
891                        stack.push(Frame::Array(Vec::new()));
892                        continue 'descend;
893                    }
894                }
895                Some(b'{') => {
896                    self.pos += 1;
897                    self.skip_whitespace();
898                    if self.peek() == Some(b'}') {
899                        self.pos += 1;
900                        node = Node::Object(Vec::new());
901                    } else {
902                        let key = self.parse_key()?;
903                        stack.push(Frame::Object {
904                            entries: Vec::new(),
905                            seen: HashMap::new(),
906                            key,
907                        });
908                        continue 'descend;
909                    }
910                }
911                _ => node = self.parse_scalar()?,
912            }
913
914            // `node` is finished: hand it to its parent, then close as many
915            // containers as end here.
916            loop {
917                let Some(frame) = stack.last_mut() else {
918                    return Ok(node);
919                };
920                let closing = match frame {
921                    Frame::Array(items) => {
922                        items.push(node);
923                        b']'
924                    }
925                    Frame::Object { entries, seen, key } => {
926                        let key = std::mem::take(key);
927                        match seen.get(&key) {
928                            Some(&at) => entries[at].1 = node,
929                            None => {
930                                seen.insert(key.clone(), entries.len());
931                                entries.push((key, node));
932                            }
933                        }
934                        b'}'
935                    }
936                };
937                self.skip_whitespace();
938                match self.peek() {
939                    Some(b',') => {
940                        self.pos += 1;
941                        if closing == b'}' {
942                            let next_key = self.parse_key()?;
943                            if let Some(Frame::Object { key, .. }) = stack.last_mut() {
944                                *key = next_key;
945                            }
946                        }
947                        continue 'descend;
948                    }
949                    Some(byte) if byte == closing => {
950                        self.pos += 1;
951                        node = match stack.pop() {
952                            Some(Frame::Array(items)) => Node::Array(items),
953                            Some(Frame::Object { entries, .. }) => Node::Object(entries),
954                            None => unreachable!("the frame was just borrowed"),
955                        };
956                    }
957                    _ if closing == b']' => return Err(self.error("expected `,` or `]`")),
958                    _ => return Err(self.error("expected `,` or `}`")),
959                }
960            }
961        }
962    }
963
964    /// Parse `"key" :`, leaving the parser on the value.
965    fn parse_key(&mut self) -> Result<PyStr> {
966        self.skip_whitespace();
967        if self.peek() != Some(b'"') {
968            return Err(self.error("key must be a string"));
969        }
970        let key = self.parse_string()?;
971        self.skip_whitespace();
972        if self.peek() != Some(b':') {
973            return Err(self.error("expected `:`"));
974        }
975        self.pos += 1;
976        Ok(key)
977    }
978
979    fn parse_scalar(&mut self) -> Result<Node> {
980        match self.peek() {
981            Some(b'"') => Ok(Node::Str(self.parse_string()?)),
982            Some(b't') => {
983                self.expect_literal("true")?;
984                Ok(Node::Bool(true))
985            }
986            Some(b'f') => {
987                self.expect_literal("false")?;
988                Ok(Node::Bool(false))
989            }
990            Some(b'n') => {
991                self.expect_literal("null")?;
992                Ok(Node::Null)
993            }
994            // Python's json emits and accepts these three (`allow_nan=True` is
995            // the default both ways), so upstream renders documents containing
996            // them instead of rejecting the file.
997            Some(b'N') => {
998                self.expect_literal("NaN")?;
999                Ok(Node::NonFinite("NaN"))
1000            }
1001            Some(b'I') => {
1002                self.expect_literal("Infinity")?;
1003                Ok(Node::NonFinite("Infinity"))
1004            }
1005            Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
1006                self.pos += "-Infinity".len();
1007                Ok(Node::NonFinite("-Infinity"))
1008            }
1009            Some(b'-' | b'0'..=b'9') => self.parse_number(),
1010            Some(_) => Err(self.error("expected value")),
1011            None => Err(self.error("EOF while parsing a value")),
1012        }
1013    }
1014
1015    /// Read a string token and decode it with `serde_json`, so escapes, lone
1016    /// surrogates and raw control characters behave exactly as before.
1017    fn parse_string(&mut self) -> Result<PyStr> {
1018        let start = self.pos;
1019        let mut end = self.pos + 1;
1020        loop {
1021            match self.bytes.get(end) {
1022                None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
1023                Some(b'"') => {
1024                    end += 1;
1025                    break;
1026                }
1027                Some(b'\\') => {
1028                    end += 1;
1029                    // Step over the escaped character whole. A multi-byte
1030                    // character after a backslash is invalid JSON, but `end`
1031                    // must still land on a UTF-8 boundary or slicing panics
1032                    // before `serde_json` gets to reject it.
1033                    match self.src[end..].chars().next() {
1034                        Some(ch) => end += ch.len_utf8(),
1035                        None => {
1036                            return Err(
1037                                self.error_at(self.bytes.len(), "EOF while parsing a string")
1038                            );
1039                        }
1040                    }
1041                }
1042                // Continuation bytes are never `"` or `\`, so scanning byte by
1043                // byte cannot mistake one for a delimiter.
1044                Some(_) => end += 1,
1045            }
1046        }
1047        let token = &self.src[start..end];
1048        let decoded = match serde_json::from_str::<String>(token) {
1049            Ok(decoded) => decoded.into_bytes(),
1050            // `serde_json` refuses lone surrogates, which Python's `json`
1051            // accepts; anything else it refuses stays an error.
1052            Err(error) => decode_with_surrogates(token)
1053                .ok_or_else(|| self.error_at(start, &describe(&error)))?,
1054        };
1055        self.pos = end;
1056        Ok(decoded)
1057    }
1058
1059    /// Validate JSON's number grammar before decoding, preserving arbitrary-size
1060    /// integers and Python's float overflow to Infinity (#74).
1061    fn parse_number(&mut self) -> Result<Node> {
1062        let start = self.pos;
1063        let mut end = start;
1064        while matches!(
1065            self.bytes.get(end),
1066            Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
1067        ) {
1068            end += 1;
1069        }
1070        let token = &self.src[start..end];
1071        let digits = token.as_bytes();
1072        let mut i = usize::from(digits.first() == Some(&b'-'));
1073        if digits.get(i) == Some(&b'0') {
1074            i += 1;
1075        } else {
1076            let first = i;
1077            while digits.get(i).is_some_and(u8::is_ascii_digit) {
1078                i += 1;
1079            }
1080            if i == first {
1081                return Err(self.error_at(start, "invalid number"));
1082            }
1083        }
1084        let mut floating = false;
1085        if digits.get(i) == Some(&b'.') {
1086            floating = true;
1087            i += 1;
1088            let first = i;
1089            while digits.get(i).is_some_and(u8::is_ascii_digit) {
1090                i += 1;
1091            }
1092            if i == first {
1093                return Err(self.error_at(start, "invalid number"));
1094            }
1095        }
1096        if matches!(digits.get(i), Some(b'e' | b'E')) {
1097            floating = true;
1098            i += 1;
1099            if matches!(digits.get(i), Some(b'+' | b'-')) {
1100                i += 1;
1101            }
1102            let first = i;
1103            while digits.get(i).is_some_and(u8::is_ascii_digit) {
1104                i += 1;
1105            }
1106            if i == first {
1107                return Err(self.error_at(start, "invalid number"));
1108            }
1109        }
1110        if i != digits.len() {
1111            return Err(self.error_at(start, "invalid number"));
1112        }
1113        self.pos = end;
1114        if !floating {
1115            return Ok(Node::Number(
1116                if token == "-0" { "0" } else { token }.to_string(),
1117            ));
1118        }
1119        let value: f64 = token
1120            .parse()
1121            .map_err(|_| self.error_at(start, "invalid number"))?;
1122        if value.is_infinite() {
1123            return Ok(Node::NonFinite(if value.is_sign_negative() {
1124                "-Infinity"
1125            } else {
1126                "Infinity"
1127            }));
1128        }
1129        // `json.dumps` writes a float with `float.__repr__`.
1130        Ok(Node::Number(crate::pyformat::float_repr(value)))
1131    }
1132
1133    fn expect_literal(&mut self, literal: &str) -> Result<()> {
1134        if self.src[self.pos..].starts_with(literal) {
1135            self.pos += literal.len();
1136            Ok(())
1137        } else {
1138            Err(self.error("expected value"))
1139        }
1140    }
1141
1142    fn peek(&self) -> Option<u8> {
1143        self.bytes.get(self.pos).copied()
1144    }
1145
1146    fn skip_whitespace(&mut self) {
1147        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
1148            self.pos += 1;
1149        }
1150    }
1151
1152    fn error(&self, message: &str) -> RichError {
1153        self.error_at(self.pos, message)
1154    }
1155
1156    fn error_at(&self, pos: usize, message: &str) -> RichError {
1157        let (line, column) = self.line_column(pos);
1158        RichError::Json(format!("{message} at line {line} column {column}"))
1159    }
1160
1161    fn line_column(&self, pos: usize) -> (usize, usize) {
1162        let mut pos = pos.min(self.src.len());
1163        while !self.src.is_char_boundary(pos) {
1164            pos -= 1;
1165        }
1166        let before = &self.src[..pos];
1167        let line = 1 + before.matches('\n').count();
1168        let column = before
1169            .rsplit('\n')
1170            .next()
1171            .map_or(0, |tail| tail.chars().count())
1172            + 1;
1173        (line, column)
1174    }
1175}
1176
1177/// `serde_json`'s message without its own `at line … column …` suffix, which
1178/// counts from the start of the token slice rather than the document.
1179fn describe(error: &serde_json::Error) -> String {
1180    let text = error.to_string();
1181    match text.find(" at line ") {
1182        Some(at) => text[..at].to_string(),
1183        None => text,
1184    }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189    use super::*;
1190    use crate::color::ColorSystem;
1191
1192    /// The spans recorded while serializing are exactly the ones upstream's
1193    /// `JSONHighlighter` regexes find, wherever `dumps` claims they are.
1194    #[test]
1195    fn recorded_spans_match_the_highlighter_regexes() {
1196        let documents = [
1197            r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
1198            r#"[1e20, -0.0, 1.5e-7, 12345678901234567890, false, [], {}, [[]], {"a": {}}]"#,
1199            r#"{"true": "false", "x:y": "a\"b", "q\"": ": null", "n": [NaN, Infinity, -Infinity]}"#,
1200            r#"{"caf\u00e9": "\u2764 \ud83d\ude00", "ctl": "\u0001\t\n", "": ""}"#,
1201            r#"["[{(1)}]", "0x1F", "b\"x", "\\\"", "tail\\x"]"#,
1202        ];
1203        let indents = [None, Some("  "), Some("\t"), Some("")];
1204        for document in documents {
1205            for indent in indents {
1206                for ensure_ascii in [false, true] {
1207                    let options = JsonOptions {
1208                        indent: indent.map(str::to_string),
1209                        ensure_ascii,
1210                        sort_keys: true,
1211                        ..JsonOptions::default()
1212                    };
1213                    let value = Parser::new(document).parse_document().unwrap();
1214                    let dumped = dumps(&value, &options);
1215                    assert!(dumped.exact, "{document} / {indent:?}");
1216                    let mut recorded = dumped.spans.clone();
1217                    recorded.extend(dumped.keys.clone());
1218                    let expected = json_highlight(dumped.json.clone());
1219                    assert_eq!(recorded, expected.spans(), "{document} / {indent:?}");
1220                }
1221            }
1222        }
1223        // A string ending in a backslash defeats `JSON_STR`, so the regexes run.
1224        let value = Parser::new(r#"{"k": "a\\"}"#).parse_document().unwrap();
1225        assert!(!dumps(&value, &JsonOptions::default()).exact);
1226        let value = Parser::new("[1]").parse_document().unwrap();
1227        let options = JsonOptions {
1228            indent: Some("1".to_string()),
1229            ..JsonOptions::default()
1230        };
1231        assert!(!dumps(&value, &options).exact);
1232    }
1233
1234    fn render(text: &str) -> String {
1235        let console = Console::builder()
1236            .force_terminal(true)
1237            .color_system(Some(ColorSystem::Truecolor))
1238            .width(40)
1239            .build();
1240        console.render_to_string(&Json::new(text).unwrap())
1241    }
1242
1243    fn render_plain(text: &str, width: usize) -> String {
1244        let console = Console::builder().width(width).color_system(None).build();
1245        console.render_to_string(&Json::new(text).expect("valid json"))
1246    }
1247
1248    #[test]
1249    fn empty_collections_stay_inline() {
1250        assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
1251        assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
1252    }
1253
1254    #[test]
1255    fn object_with_scalars() {
1256        assert_eq!(
1257            render(r#"{"ok": false}"#),
1258            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
1259        );
1260    }
1261
1262    #[test]
1263    fn invalid_json_errors() {
1264        assert!(Json::new("{not json}").is_err());
1265    }
1266
1267    #[test]
1268    fn non_ascii_stays_utf8_in_input_order() {
1269        // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
1270        // characters render as UTF-8 (not \uXXXX), and keys keep input order.
1271        // (Byte-parity is guaranteed by the `json_unicode` golden.)
1272        let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
1273        assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
1274        assert!(out.contains('\u{2764}'), "heart stays UTF-8");
1275        let name_at = out.find("name").expect("name key present");
1276        let emoji_at = out.find("emoji").expect("emoji key present");
1277        assert!(name_at < emoji_at, "keys keep input order");
1278    }
1279
1280    #[test]
1281    fn a_long_value_is_wrapped_rather_than_cropped() {
1282        // A long string value used to be cut mid-token, so the printed document
1283        // was missing data -- and, for JSON, no longer parseable -- at exit 0.
1284        let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
1285        let out = render_plain(&payload, 40);
1286        assert_eq!(
1287            out.matches('y').count(),
1288            120,
1289            "characters were dropped:
1290{out}"
1291        );
1292    }
1293
1294    /// Upstream wraps the JSON at word boundaries: `JSON.text` asks for
1295    /// `no_wrap`, but `Console.print` re-joins it through `Text(sep).join(...)`
1296    /// and the joined copy carries neither the flag nor an overflow, so the
1297    /// default `fold` word wrap applies. Character folding split words
1298    /// (`over t` / `he lazy`), which no upstream output ever shows.
1299    ///
1300    /// Captured from rich 15.0.0:
1301    /// `Console(width=40).print(JSON(...))`.
1302    #[test]
1303    fn wrapping_breaks_at_word_boundaries() {
1304        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
1305        assert_eq!(
1306            render_plain(payload, 40),
1307            "{\n  \"k\": \"the quick brown fox jumps over \n\
1308             the lazy dog and keeps running for a \n\
1309             very long time indeed\"\n}"
1310        );
1311    }
1312
1313    #[cfg(feature = "json-escape-safe")]
1314    #[test]
1315    fn adjusted_escape_boundaries_preserve_the_remaining_payload() {
1316        let input = format!(r#"{{"v":"aa\u0001{}"}}"#, "b".repeat(40));
1317        for width in 6..=20 {
1318            let output = Console::builder()
1319                .width(width)
1320                .force_terminal(false)
1321                .build()
1322                .render_to_string(&Json::new(&input).unwrap().escape_safe(true));
1323            assert_eq!(output.matches('b').count(), 40, "width {width}: {output:?}");
1324        }
1325    }
1326
1327    #[cfg(feature = "json-escape-safe")]
1328    #[test]
1329    fn wrapping_keeps_json_escapes_atomic_at_narrow_widths() {
1330        let payload = r#"{"v":"a\"b\\c\nd\u0001e"}"#;
1331        for width in 8..=14 {
1332            let output = Console::builder()
1333                .width(width)
1334                .force_terminal(false)
1335                .build()
1336                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
1337            for line in output.lines() {
1338                let bytes = line.as_bytes();
1339                let mut index = 0;
1340                while index < bytes.len() {
1341                    if bytes[index] != b'\\' {
1342                        index += 1;
1343                        continue;
1344                    }
1345                    assert!(
1346                        index + 1 < bytes.len(),
1347                        "split escape at width {width}: {output:?}"
1348                    );
1349                    if bytes[index + 1] == b'u' {
1350                        assert!(
1351                            index + 6 <= bytes.len(),
1352                            "split unicode escape at width {width}: {output:?}"
1353                        );
1354                        index += 6;
1355                    } else {
1356                        index += 2;
1357                    }
1358                }
1359            }
1360        }
1361    }
1362
1363    #[cfg(feature = "json-escape-safe")]
1364    #[test]
1365    fn escape_folding_preserves_bytes_even_below_the_escape_width() {
1366        let payload = r#"{"v":"a\"b\\c\nd\u0001eeeeeeeeeeee"}"#;
1367        let wide = Console::builder()
1368            .width(100)
1369            .force_terminal(false)
1370            .build()
1371            .render_to_string(&Json::new(payload).unwrap().escape_safe(true))
1372            .replace('\n', "");
1373        for width in 1..=20 {
1374            let output = Console::builder()
1375                .width(width)
1376                .force_terminal(false)
1377                .build()
1378                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
1379            assert_eq!(output.replace('\n', ""), wide, "width {width}");
1380            assert!(
1381                output
1382                    .lines()
1383                    .all(|line| crate::cells::cell_len(line) <= width)
1384            );
1385        }
1386    }
1387
1388    #[cfg(feature = "json-escape-safe")]
1389    #[test]
1390    fn escape_cropping_never_emits_a_partial_escape() {
1391        let payload = r#""a\"b\\c\nd\u0001eeee""#;
1392        for width in 1..=24 {
1393            let output = Console::builder()
1394                .width(width)
1395                .force_terminal(false)
1396                .build()
1397                .render_to_string(&Json::new(payload).unwrap().no_wrap(true).escape_safe(true));
1398            let mut chars = output.chars();
1399            while let Some(c) = chars.next() {
1400                if c == '\\' {
1401                    let next = chars.next().expect("complete short escape");
1402                    if next == 'u' {
1403                        for _ in 0..4 {
1404                            assert!(chars.next().is_some_and(|c| c.is_ascii_hexdigit()));
1405                        }
1406                    }
1407                }
1408            }
1409        }
1410    }
1411
1412    /// Nested inside another renderable, `JSON.text.no_wrap` survives and each
1413    /// line is **cropped** at the width rather than wrapped — see
1414    /// [`Json::no_wrap`]. Wrapping here instead was silent content loss: with
1415    /// `rich -j doc.json -w 120` on an 80-column console the document was laid
1416    /// out at 120 and then cropped to 80 by `Console.print`, so whole runs
1417    /// vanished and the surviving text read as if it were contiguous.
1418    ///
1419    /// Captured from rich-cli 1.8.1 driven by rich 15.0.0:
1420    /// `COLUMNS=80 rich -j long.json -w 40`.
1421    #[test]
1422    fn a_nested_document_is_cropped_rather_than_wrapped() {
1423        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
1424        let console = Console::builder().width(40).color_system(None).build();
1425        let json = Json::new(payload).expect("valid json").no_wrap(true);
1426        assert_eq!(
1427            console.render_to_string(&json),
1428            "{\n  \"k\": \"the quick brown fox jumps over t\n}"
1429        );
1430
1431        // …and the wrap is still the default, because a bare
1432        // `Console.print(JSON(...))` loses the flag in `Text.join`.
1433        assert_eq!(
1434            render_plain(payload, 40),
1435            "{\n  \"k\": \"the quick brown fox jumps over \n\
1436             the lazy dog and keeps running for a \n\
1437             very long time indeed\"\n}"
1438        );
1439    }
1440
1441    /// A crop must not cut a double-width character in half: `set_cell_size`
1442    /// drops the straddling character and the line comes out one cell short,
1443    /// never one cell over.
1444    #[test]
1445    fn cropping_never_splits_a_wide_character() {
1446        let console = Console::builder().width(12).color_system(None).build();
1447        let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
1448            .expect("valid json")
1449            .no_wrap(true);
1450        for line in console.render_to_string(&json).lines() {
1451            assert!(
1452                crate::cells::cell_len(line) <= 12,
1453                "line {line:?} overflows the crop"
1454            );
1455        }
1456    }
1457
1458    /// serde_json's default float parser takes a fast path that can land 1 ULP
1459    /// from the value in the file, so the rendered number parsed back to a
1460    /// *different* double. The `float_roundtrip` feature makes parsing exact.
1461    #[test]
1462    fn floats_round_trip_exactly() {
1463        for literal in [
1464            "-938371.9565467801",
1465            "0.1",
1466            "1.7976931348623157e308",
1467            "5e-324",
1468            "3.141592653589793",
1469        ] {
1470            let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
1471            let rendered: String = out
1472                .split(':')
1473                .nth(1)
1474                .expect("a value after the key")
1475                .trim()
1476                .trim_end_matches(['}', ' ', '\n'])
1477                .to_string();
1478            let want: f64 = literal.parse().expect("literal parses");
1479            let got: f64 = rendered
1480                .parse()
1481                .unwrap_or_else(|_| panic!("rendered {rendered:?}"));
1482            assert_eq!(
1483                got.to_bits(),
1484                want.to_bits(),
1485                "{literal} rendered as {rendered} — a different double"
1486            );
1487        }
1488    }
1489
1490    /// serde_json stops at 128 levels, so a 200-deep document — which CPython
1491    /// parses without complaint — was reported as invalid JSON and the CLI
1492    /// exited 1 on a file upstream renders.
1493    #[test]
1494    fn deep_nesting_is_not_rejected() {
1495        for depth in [128, 129, 200, 1000] {
1496            let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
1497            let json = Json::new(&payload)
1498                .unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
1499            let console = Console::builder()
1500                .width(4 * depth + 8)
1501                .color_system(None)
1502                .build();
1503            let out = console.render_to_string(&json);
1504            assert_eq!(
1505                out.matches('[').count(),
1506                depth,
1507                "depth {depth} did not render every level"
1508            );
1509        }
1510    }
1511
1512    /// Parsing and dropping must cost heap, not stack: a recursive parser (or
1513    /// the compiler's recursive drop glue) turns a deep document into a stack
1514    /// overflow, which kills the process without even an error message.
1515    #[test]
1516    fn very_deep_nesting_does_not_overflow_the_stack() {
1517        let payload = format!("{}1{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
1518        let json = Json::new(&payload).expect("deep document parses");
1519        drop(json);
1520        // Drop glue on a tree far past the parse limit stays iterative too.
1521        let mut node = Node::Number("1".to_string());
1522        for _ in 0..100_000 {
1523            node = Node::Array(vec![node]);
1524        }
1525        drop(node);
1526    }
1527
1528    /// Past `MAX_DEPTH` the document is refused with CPython's
1529    /// `RecursionError` message instead of being rendered: its indentation
1530    /// alone is quadratic in the depth, and a 100 000-deep array exhausted
1531    /// memory and was killed.
1532    #[test]
1533    fn nesting_past_the_limit_is_an_error() {
1534        for (open, close, what) in [("[", "]", "array"), ("{\"a\":", "}", "object")] {
1535            for depth in [MAX_DEPTH + 1, 100_000] {
1536                let payload = format!("{}1{}", open.repeat(depth), close.repeat(depth));
1537                let error = Json::new(&payload).err().expect("too deep");
1538                assert!(
1539                    error.to_string().contains(&format!(
1540                        "maximum recursion depth exceeded while decoding a JSON {what}"
1541                    )),
1542                    "{error}"
1543                );
1544            }
1545        }
1546        // An empty container counts as a level, as a call in CPython's scanner.
1547        let payload = format!("{}[]{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
1548        assert!(Json::new(&payload).is_err());
1549        let payload = format!(
1550            "{}[]{}",
1551            "[".repeat(MAX_DEPTH - 1),
1552            "]".repeat(MAX_DEPTH - 1)
1553        );
1554        assert!(Json::new(&payload).is_ok());
1555    }
1556
1557    /// Python's json accepts and emits `NaN` / `Infinity` / `-Infinity`
1558    /// (`allow_nan=True` is the default), and rich's JSONHighlighter has no
1559    /// rule that matches them, so upstream prints them *unstyled*. serde_json
1560    /// rejected the whole document.
1561    ///
1562    /// Captured from rich 15.0.0:
1563    /// `Console(width=40, force_terminal=True).print(JSON(...))`.
1564    #[test]
1565    fn non_finite_numbers_render_unstyled() {
1566        assert_eq!(
1567            render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
1568            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"a\"\x1b[0m: NaN,\n  \x1b[1;34m\"b\"\x1b[0m: \
1569             Infinity,\n  \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n  \x1b[1;34m\"d\"\x1b[0m: \
1570             \x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
1571        );
1572        // Python spells them with those exact capitalisations and nothing else.
1573        for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
1574            assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
1575        }
1576    }
1577
1578    /// The hand-written reader must accept and reject exactly what serde_json
1579    /// does for bounded numbers — Python also accepts overflowing exponents.
1580    #[test]
1581    fn acceptance_matches_serde_json() {
1582        let samples = [
1583            "{}",
1584            "[]",
1585            "  {\t\"a\" :\n1 }  ",
1586            r#"{"a": 1, "a": 2}"#,
1587            r#"{"a": [1, {"b": null}], "c": "x"}"#,
1588            "0",
1589            "-0",
1590            "0.0",
1591            "1e10",
1592            "1E+10",
1593            "1e-7",
1594            "12345678901234567890",
1595            "-12345678901234567890123456789012345",
1596            "01",
1597            "1.",
1598            ".1",
1599            "+1",
1600            "1e",
1601            "-",
1602            "--1",
1603            "-i",
1604            "-Inf",
1605            "Infinit",
1606            "NAN",
1607            "1 2",
1608            "",
1609            "   ",
1610            "{",
1611            "[",
1612            "]",
1613            "}",
1614            "[,]",
1615            "[1,]",
1616            r#"{"a": 1,}"#,
1617            r#"{a: 1}"#,
1618            r#"{'a': 1}"#,
1619            r#"{"a" 1}"#,
1620            "[1 2]",
1621            "truex",
1622            "tru",
1623            "nul",
1624            r#""unterminated"#,
1625            r#""\q""#,
1626            r#""é""#,
1627            r#""😀""#,
1628            "\"raw\nnewline\"",
1629            r#""café ❤""#,
1630            "\"\u{e9}\\\"",
1631            "\u{feff}{}",
1632            "[[[[1]]]]",
1633        ];
1634        for sample in samples {
1635            let ours = Json::new(sample).is_ok();
1636            let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
1637            assert_eq!(ours, theirs, "disagreed about {sample:?}");
1638        }
1639    }
1640
1641    #[test]
1642    fn python_numbers_preserve_large_integers_and_overflow() {
1643        for number in [
1644            "1234567890123456789012345678901234567890",
1645            "-1234567890123456789012345678901234567890",
1646        ] {
1647            assert_eq!(render_plain(number, 100), number);
1648        }
1649        assert_eq!(render_plain("-0", 100), "0");
1650        assert_eq!(render_plain("1e400", 100), "Infinity");
1651        assert_eq!(render_plain("-1e999", 100), "-Infinity");
1652        for invalid in ["01", "-01", "1.e2", "1e+", "1e400x", "--1", "1+2", ".1"] {
1653            assert!(Json::new(invalid).is_err(), "accepted {invalid}");
1654        }
1655    }
1656
1657    /// And the tree it builds must be the tree serde_json would have built.
1658    /// `serde_json::to_string_pretty` happens to use the very layout upstream's
1659    /// `json.dumps(indent=2)` does, so it doubles as a reference dump: key
1660    /// order, repeated-key collapsing, string escaping and number formatting
1661    /// all have to agree.
1662    #[test]
1663    fn the_parsed_tree_matches_serde_json() {
1664        let samples = [
1665            r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
1666            r#"{"a": 1, "b": 2, "a": 3}"#,
1667            r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
1668            r#"{"k": "A\t\"x\"A\\\/é"}"#,
1669            r#"[0, 12345678901234567890, -7]"#,
1670            r#"{"café": "❤", "": ""}"#,
1671            "[]",
1672            "{}",
1673            "\"top level\"",
1674            "1234",
1675        ];
1676        for sample in samples {
1677            let reference: serde_json::Value =
1678                serde_json::from_str(sample).expect("sample is valid JSON");
1679            assert_eq!(
1680                render_plain(sample, 10_000),
1681                serde_json::to_string_pretty(&reference).expect("value re-serialises"),
1682                "diverged on {sample}"
1683            );
1684        }
1685    }
1686
1687    /// Floats are written with Python's `float.__repr__`, as `json.dumps` does,
1688    /// not serde_json's spelling (`1e-7`, `10000000000.0`).
1689    #[test]
1690    fn floats_render_as_python_repr() {
1691        let out = render_plain(
1692            "[-0.5, 1e10, 1E+10, 1e-7, 1.7976931348623157e308, 1e16]",
1693            10_000,
1694        );
1695        let values: Vec<&str> = out
1696            .lines()
1697            .filter_map(|line| line.trim().strip_suffix(',').or(Some(line.trim())))
1698            .filter(|value| !matches!(*value, "[" | "]"))
1699            .collect();
1700        assert_eq!(
1701            values,
1702            [
1703                "-0.5",
1704                "10000000000.0",
1705                "10000000000.0",
1706                "1e-07",
1707                "1.7976931348623157e+308",
1708                "1e+16"
1709            ]
1710        );
1711    }
1712
1713    /// A repeated key collapses to one entry — first position, last value —
1714    /// which is what both `dict` and serde_json's `preserve_order` produce.
1715    #[test]
1716    fn a_repeated_key_keeps_its_position_and_last_value() {
1717        assert_eq!(
1718            render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
1719            "{\n  \"a\": 3,\n  \"b\": 2\n}"
1720        );
1721    }
1722
1723    /// Escapes are decoded and re-encoded, because upstream re-serialises the
1724    /// parsed data with `json.dumps`.
1725    #[test]
1726    fn escapes_are_re_encoded_like_dumps() {
1727        assert_eq!(
1728            render_plain(r#"{"k": "A\t\"x\""}"#, 60),
1729            "{\n  \"k\": \"A\\t\\\"x\\\"\"\n}"
1730        );
1731    }
1732}