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            if let Some(literal) = first_non_finite(&value) {
200                return Err(RichError::Json(format!(
201                    "Out of range float values are not JSON compliant: {}",
202                    match literal {
203                        "NaN" => "nan",
204                        "Infinity" => "inf",
205                        _ => "-inf",
206                    }
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('\\') && lookahead.next() == Some('u') {
794                        if let Some(low @ 0xDC00..=0xDFFF) = hex4(&mut lookahead) {
795                            unit = 0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00);
796                            chars = lookahead;
797                        }
798                    }
799                }
800                match char::from_u32(unit) {
801                    Some(c) => c,
802                    None => {
803                        // A lone surrogate: its generalized UTF-8 encoding.
804                        out.push(0xE0 | (unit >> 12) as u8);
805                        out.push(0x80 | ((unit >> 6) & 0x3F) as u8);
806                        out.push(0x80 | (unit & 0x3F) as u8);
807                        continue;
808                    }
809                }
810            }
811            _ => return None,
812        };
813        let mut buf = [0u8; 4];
814        out.extend_from_slice(decoded.encode_utf8(&mut buf).as_bytes());
815    }
816    Some(out)
817}
818
819fn quote(string: &str) -> String {
820    serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
821}
822
823/// A container being filled in, held on the parser's explicit stack.
824enum Frame {
825    Array(Vec<Node>),
826    Object {
827        entries: Vec<(PyStr, Node)>,
828        /// Key -> position in `entries`, so a repeated key overwrites in place
829        /// (`{"a": 1, "a": 2}` is one entry) without an O(n^2) rescan. Dropped
830        /// with the frame, so only the objects on the current path pay for it.
831        seen: HashMap<PyStr, usize>,
832        /// The key whose value is currently being parsed.
833        key: PyStr,
834    },
835}
836
837/// A non-recursive JSON reader. Structure is walked with an explicit stack;
838/// scalar tokens are handed to `serde_json` for decoding so escapes, number
839/// formats and their rejections stay identical to the rest of the workspace.
840struct Parser<'a> {
841    src: &'a str,
842    bytes: &'a [u8],
843    pos: usize,
844}
845
846impl<'a> Parser<'a> {
847    fn new(src: &'a str) -> Self {
848        Parser {
849            src,
850            bytes: src.as_bytes(),
851            pos: 0,
852        }
853    }
854
855    fn parse_document(&mut self) -> Result<Node> {
856        let value = self.parse_value()?;
857        self.skip_whitespace();
858        if self.pos != self.bytes.len() {
859            return Err(self.error("trailing characters"));
860        }
861        Ok(value)
862    }
863
864    /// Parse one value, descending into containers with an explicit stack.
865    fn parse_value(&mut self) -> Result<Node> {
866        let mut stack: Vec<Frame> = Vec::new();
867        let mut node: Node;
868
869        'descend: loop {
870            self.skip_whitespace();
871            // CPython's scanner enters a recursive call per container, empty
872            // ones included, and raises once the recursion limit is reached.
873            if let Some(kind @ (b'[' | b'{')) = self.peek() {
874                if stack.len() >= MAX_DEPTH {
875                    let what = if kind == b'[' { "array" } else { "object" };
876                    return Err(self.error(&format!(
877                        "maximum recursion depth exceeded while decoding a JSON {what} \
878                         from a unicode string"
879                    )));
880                }
881            }
882            match self.peek() {
883                Some(b'[') => {
884                    self.pos += 1;
885                    self.skip_whitespace();
886                    if self.peek() == Some(b']') {
887                        self.pos += 1;
888                        node = Node::Array(Vec::new());
889                    } else {
890                        stack.push(Frame::Array(Vec::new()));
891                        continue 'descend;
892                    }
893                }
894                Some(b'{') => {
895                    self.pos += 1;
896                    self.skip_whitespace();
897                    if self.peek() == Some(b'}') {
898                        self.pos += 1;
899                        node = Node::Object(Vec::new());
900                    } else {
901                        let key = self.parse_key()?;
902                        stack.push(Frame::Object {
903                            entries: Vec::new(),
904                            seen: HashMap::new(),
905                            key,
906                        });
907                        continue 'descend;
908                    }
909                }
910                _ => node = self.parse_scalar()?,
911            }
912
913            // `node` is finished: hand it to its parent, then close as many
914            // containers as end here.
915            loop {
916                let Some(frame) = stack.last_mut() else {
917                    return Ok(node);
918                };
919                let closing = match frame {
920                    Frame::Array(items) => {
921                        items.push(node);
922                        b']'
923                    }
924                    Frame::Object { entries, seen, key } => {
925                        let key = std::mem::take(key);
926                        match seen.get(&key) {
927                            Some(&at) => entries[at].1 = node,
928                            None => {
929                                seen.insert(key.clone(), entries.len());
930                                entries.push((key, node));
931                            }
932                        }
933                        b'}'
934                    }
935                };
936                self.skip_whitespace();
937                match self.peek() {
938                    Some(b',') => {
939                        self.pos += 1;
940                        if closing == b'}' {
941                            let next_key = self.parse_key()?;
942                            if let Some(Frame::Object { key, .. }) = stack.last_mut() {
943                                *key = next_key;
944                            }
945                        }
946                        continue 'descend;
947                    }
948                    Some(byte) if byte == closing => {
949                        self.pos += 1;
950                        node = match stack.pop() {
951                            Some(Frame::Array(items)) => Node::Array(items),
952                            Some(Frame::Object { entries, .. }) => Node::Object(entries),
953                            None => unreachable!("the frame was just borrowed"),
954                        };
955                    }
956                    _ if closing == b']' => return Err(self.error("expected `,` or `]`")),
957                    _ => return Err(self.error("expected `,` or `}`")),
958                }
959            }
960        }
961    }
962
963    /// Parse `"key" :`, leaving the parser on the value.
964    fn parse_key(&mut self) -> Result<PyStr> {
965        self.skip_whitespace();
966        if self.peek() != Some(b'"') {
967            return Err(self.error("key must be a string"));
968        }
969        let key = self.parse_string()?;
970        self.skip_whitespace();
971        if self.peek() != Some(b':') {
972            return Err(self.error("expected `:`"));
973        }
974        self.pos += 1;
975        Ok(key)
976    }
977
978    fn parse_scalar(&mut self) -> Result<Node> {
979        match self.peek() {
980            Some(b'"') => Ok(Node::Str(self.parse_string()?)),
981            Some(b't') => {
982                self.expect_literal("true")?;
983                Ok(Node::Bool(true))
984            }
985            Some(b'f') => {
986                self.expect_literal("false")?;
987                Ok(Node::Bool(false))
988            }
989            Some(b'n') => {
990                self.expect_literal("null")?;
991                Ok(Node::Null)
992            }
993            // Python's json emits and accepts these three (`allow_nan=True` is
994            // the default both ways), so upstream renders documents containing
995            // them instead of rejecting the file.
996            Some(b'N') => {
997                self.expect_literal("NaN")?;
998                Ok(Node::NonFinite("NaN"))
999            }
1000            Some(b'I') => {
1001                self.expect_literal("Infinity")?;
1002                Ok(Node::NonFinite("Infinity"))
1003            }
1004            Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
1005                self.pos += "-Infinity".len();
1006                Ok(Node::NonFinite("-Infinity"))
1007            }
1008            Some(b'-' | b'0'..=b'9') => self.parse_number(),
1009            Some(_) => Err(self.error("expected value")),
1010            None => Err(self.error("EOF while parsing a value")),
1011        }
1012    }
1013
1014    /// Read a string token and decode it with `serde_json`, so escapes, lone
1015    /// surrogates and raw control characters behave exactly as before.
1016    fn parse_string(&mut self) -> Result<PyStr> {
1017        let start = self.pos;
1018        let mut end = self.pos + 1;
1019        loop {
1020            match self.bytes.get(end) {
1021                None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
1022                Some(b'"') => {
1023                    end += 1;
1024                    break;
1025                }
1026                Some(b'\\') => {
1027                    end += 1;
1028                    // Step over the escaped character whole. A multi-byte
1029                    // character after a backslash is invalid JSON, but `end`
1030                    // must still land on a UTF-8 boundary or slicing panics
1031                    // before `serde_json` gets to reject it.
1032                    match self.src[end..].chars().next() {
1033                        Some(ch) => end += ch.len_utf8(),
1034                        None => {
1035                            return Err(
1036                                self.error_at(self.bytes.len(), "EOF while parsing a string")
1037                            )
1038                        }
1039                    }
1040                }
1041                // Continuation bytes are never `"` or `\`, so scanning byte by
1042                // byte cannot mistake one for a delimiter.
1043                Some(_) => end += 1,
1044            }
1045        }
1046        let token = &self.src[start..end];
1047        let decoded = match serde_json::from_str::<String>(token) {
1048            Ok(decoded) => decoded.into_bytes(),
1049            // `serde_json` refuses lone surrogates, which Python's `json`
1050            // accepts; anything else it refuses stays an error.
1051            Err(error) => decode_with_surrogates(token)
1052                .ok_or_else(|| self.error_at(start, &describe(&error)))?,
1053        };
1054        self.pos = end;
1055        Ok(decoded)
1056    }
1057
1058    /// Validate JSON's number grammar before decoding, preserving arbitrary-size
1059    /// integers and Python's float overflow to Infinity (#74).
1060    fn parse_number(&mut self) -> Result<Node> {
1061        let start = self.pos;
1062        let mut end = start;
1063        while matches!(
1064            self.bytes.get(end),
1065            Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
1066        ) {
1067            end += 1;
1068        }
1069        let token = &self.src[start..end];
1070        let digits = token.as_bytes();
1071        let mut i = usize::from(digits.first() == Some(&b'-'));
1072        if digits.get(i) == Some(&b'0') {
1073            i += 1;
1074        } else {
1075            let first = i;
1076            while digits.get(i).is_some_and(u8::is_ascii_digit) {
1077                i += 1;
1078            }
1079            if i == first {
1080                return Err(self.error_at(start, "invalid number"));
1081            }
1082        }
1083        let mut floating = false;
1084        if digits.get(i) == Some(&b'.') {
1085            floating = true;
1086            i += 1;
1087            let first = i;
1088            while digits.get(i).is_some_and(u8::is_ascii_digit) {
1089                i += 1;
1090            }
1091            if i == first {
1092                return Err(self.error_at(start, "invalid number"));
1093            }
1094        }
1095        if matches!(digits.get(i), Some(b'e' | b'E')) {
1096            floating = true;
1097            i += 1;
1098            if matches!(digits.get(i), Some(b'+' | b'-')) {
1099                i += 1;
1100            }
1101            let first = i;
1102            while digits.get(i).is_some_and(u8::is_ascii_digit) {
1103                i += 1;
1104            }
1105            if i == first {
1106                return Err(self.error_at(start, "invalid number"));
1107            }
1108        }
1109        if i != digits.len() {
1110            return Err(self.error_at(start, "invalid number"));
1111        }
1112        self.pos = end;
1113        if !floating {
1114            return Ok(Node::Number(
1115                if token == "-0" { "0" } else { token }.to_string(),
1116            ));
1117        }
1118        let value: f64 = token
1119            .parse()
1120            .map_err(|_| self.error_at(start, "invalid number"))?;
1121        if value.is_infinite() {
1122            return Ok(Node::NonFinite(if value.is_sign_negative() {
1123                "-Infinity"
1124            } else {
1125                "Infinity"
1126            }));
1127        }
1128        // `json.dumps` writes a float with `float.__repr__`.
1129        Ok(Node::Number(crate::pyformat::float_repr(value)))
1130    }
1131
1132    fn expect_literal(&mut self, literal: &str) -> Result<()> {
1133        if self.src[self.pos..].starts_with(literal) {
1134            self.pos += literal.len();
1135            Ok(())
1136        } else {
1137            Err(self.error("expected value"))
1138        }
1139    }
1140
1141    fn peek(&self) -> Option<u8> {
1142        self.bytes.get(self.pos).copied()
1143    }
1144
1145    fn skip_whitespace(&mut self) {
1146        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
1147            self.pos += 1;
1148        }
1149    }
1150
1151    fn error(&self, message: &str) -> RichError {
1152        self.error_at(self.pos, message)
1153    }
1154
1155    fn error_at(&self, pos: usize, message: &str) -> RichError {
1156        let (line, column) = self.line_column(pos);
1157        RichError::Json(format!("{message} at line {line} column {column}"))
1158    }
1159
1160    fn line_column(&self, pos: usize) -> (usize, usize) {
1161        let mut pos = pos.min(self.src.len());
1162        while !self.src.is_char_boundary(pos) {
1163            pos -= 1;
1164        }
1165        let before = &self.src[..pos];
1166        let line = 1 + before.matches('\n').count();
1167        let column = before
1168            .rsplit('\n')
1169            .next()
1170            .map_or(0, |tail| tail.chars().count())
1171            + 1;
1172        (line, column)
1173    }
1174}
1175
1176/// `serde_json`'s message without its own `at line … column …` suffix, which
1177/// counts from the start of the token slice rather than the document.
1178fn describe(error: &serde_json::Error) -> String {
1179    let text = error.to_string();
1180    match text.find(" at line ") {
1181        Some(at) => text[..at].to_string(),
1182        None => text,
1183    }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188    use super::*;
1189    use crate::color::ColorSystem;
1190
1191    /// The spans recorded while serializing are exactly the ones upstream's
1192    /// `JSONHighlighter` regexes find, wherever `dumps` claims they are.
1193    #[test]
1194    fn recorded_spans_match_the_highlighter_regexes() {
1195        let documents = [
1196            r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
1197            r#"[1e20, -0.0, 1.5e-7, 12345678901234567890, false, [], {}, [[]], {"a": {}}]"#,
1198            r#"{"true": "false", "x:y": "a\"b", "q\"": ": null", "n": [NaN, Infinity, -Infinity]}"#,
1199            r#"{"caf\u00e9": "\u2764 \ud83d\ude00", "ctl": "\u0001\t\n", "": ""}"#,
1200            r#"["[{(1)}]", "0x1F", "b\"x", "\\\"", "tail\\x"]"#,
1201        ];
1202        let indents = [None, Some("  "), Some("\t"), Some("")];
1203        for document in documents {
1204            for indent in indents {
1205                for ensure_ascii in [false, true] {
1206                    let options = JsonOptions {
1207                        indent: indent.map(str::to_string),
1208                        ensure_ascii,
1209                        sort_keys: true,
1210                        ..JsonOptions::default()
1211                    };
1212                    let value = Parser::new(document).parse_document().unwrap();
1213                    let dumped = dumps(&value, &options);
1214                    assert!(dumped.exact, "{document} / {indent:?}");
1215                    let mut recorded = dumped.spans.clone();
1216                    recorded.extend(dumped.keys.clone());
1217                    let expected = json_highlight(dumped.json.clone());
1218                    assert_eq!(recorded, expected.spans(), "{document} / {indent:?}");
1219                }
1220            }
1221        }
1222        // A string ending in a backslash defeats `JSON_STR`, so the regexes run.
1223        let value = Parser::new(r#"{"k": "a\\"}"#).parse_document().unwrap();
1224        assert!(!dumps(&value, &JsonOptions::default()).exact);
1225        let value = Parser::new("[1]").parse_document().unwrap();
1226        let options = JsonOptions {
1227            indent: Some("1".to_string()),
1228            ..JsonOptions::default()
1229        };
1230        assert!(!dumps(&value, &options).exact);
1231    }
1232
1233    fn render(text: &str) -> String {
1234        let console = Console::builder()
1235            .force_terminal(true)
1236            .color_system(Some(ColorSystem::Truecolor))
1237            .width(40)
1238            .build();
1239        console.render_to_string(&Json::new(text).unwrap())
1240    }
1241
1242    fn render_plain(text: &str, width: usize) -> String {
1243        let console = Console::builder().width(width).color_system(None).build();
1244        console.render_to_string(&Json::new(text).expect("valid json"))
1245    }
1246
1247    #[test]
1248    fn empty_collections_stay_inline() {
1249        assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
1250        assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
1251    }
1252
1253    #[test]
1254    fn object_with_scalars() {
1255        assert_eq!(
1256            render(r#"{"ok": false}"#),
1257            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
1258        );
1259    }
1260
1261    #[test]
1262    fn invalid_json_errors() {
1263        assert!(Json::new("{not json}").is_err());
1264    }
1265
1266    #[test]
1267    fn non_ascii_stays_utf8_in_input_order() {
1268        // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
1269        // characters render as UTF-8 (not \uXXXX), and keys keep input order.
1270        // (Byte-parity is guaranteed by the `json_unicode` golden.)
1271        let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
1272        assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
1273        assert!(out.contains('\u{2764}'), "heart stays UTF-8");
1274        let name_at = out.find("name").expect("name key present");
1275        let emoji_at = out.find("emoji").expect("emoji key present");
1276        assert!(name_at < emoji_at, "keys keep input order");
1277    }
1278
1279    #[test]
1280    fn a_long_value_is_wrapped_rather_than_cropped() {
1281        // A long string value used to be cut mid-token, so the printed document
1282        // was missing data -- and, for JSON, no longer parseable -- at exit 0.
1283        let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
1284        let out = render_plain(&payload, 40);
1285        assert_eq!(
1286            out.matches('y').count(),
1287            120,
1288            "characters were dropped:
1289{out}"
1290        );
1291    }
1292
1293    /// Upstream wraps the JSON at word boundaries: `JSON.text` asks for
1294    /// `no_wrap`, but `Console.print` re-joins it through `Text(sep).join(...)`
1295    /// and the joined copy carries neither the flag nor an overflow, so the
1296    /// default `fold` word wrap applies. Character folding split words
1297    /// (`over t` / `he lazy`), which no upstream output ever shows.
1298    ///
1299    /// Captured from rich 15.0.0:
1300    /// `Console(width=40).print(JSON(...))`.
1301    #[test]
1302    fn wrapping_breaks_at_word_boundaries() {
1303        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
1304        assert_eq!(
1305            render_plain(payload, 40),
1306            "{\n  \"k\": \"the quick brown fox jumps over \n\
1307             the lazy dog and keeps running for a \n\
1308             very long time indeed\"\n}"
1309        );
1310    }
1311
1312    #[cfg(feature = "json-escape-safe")]
1313    #[test]
1314    fn adjusted_escape_boundaries_preserve_the_remaining_payload() {
1315        let input = format!(r#"{{"v":"aa\u0001{}"}}"#, "b".repeat(40));
1316        for width in 6..=20 {
1317            let output = Console::builder()
1318                .width(width)
1319                .force_terminal(false)
1320                .build()
1321                .render_to_string(&Json::new(&input).unwrap().escape_safe(true));
1322            assert_eq!(output.matches('b').count(), 40, "width {width}: {output:?}");
1323        }
1324    }
1325
1326    #[cfg(feature = "json-escape-safe")]
1327    #[test]
1328    fn wrapping_keeps_json_escapes_atomic_at_narrow_widths() {
1329        let payload = r#"{"v":"a\"b\\c\nd\u0001e"}"#;
1330        for width in 8..=14 {
1331            let output = Console::builder()
1332                .width(width)
1333                .force_terminal(false)
1334                .build()
1335                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
1336            for line in output.lines() {
1337                let bytes = line.as_bytes();
1338                let mut index = 0;
1339                while index < bytes.len() {
1340                    if bytes[index] != b'\\' {
1341                        index += 1;
1342                        continue;
1343                    }
1344                    assert!(
1345                        index + 1 < bytes.len(),
1346                        "split escape at width {width}: {output:?}"
1347                    );
1348                    if bytes[index + 1] == b'u' {
1349                        assert!(
1350                            index + 6 <= bytes.len(),
1351                            "split unicode escape at width {width}: {output:?}"
1352                        );
1353                        index += 6;
1354                    } else {
1355                        index += 2;
1356                    }
1357                }
1358            }
1359        }
1360    }
1361
1362    #[cfg(feature = "json-escape-safe")]
1363    #[test]
1364    fn escape_folding_preserves_bytes_even_below_the_escape_width() {
1365        let payload = r#"{"v":"a\"b\\c\nd\u0001eeeeeeeeeeee"}"#;
1366        let wide = Console::builder()
1367            .width(100)
1368            .force_terminal(false)
1369            .build()
1370            .render_to_string(&Json::new(payload).unwrap().escape_safe(true))
1371            .replace('\n', "");
1372        for width in 1..=20 {
1373            let output = Console::builder()
1374                .width(width)
1375                .force_terminal(false)
1376                .build()
1377                .render_to_string(&Json::new(payload).unwrap().escape_safe(true));
1378            assert_eq!(output.replace('\n', ""), wide, "width {width}");
1379            assert!(output
1380                .lines()
1381                .all(|line| crate::cells::cell_len(line) <= width));
1382        }
1383    }
1384
1385    #[cfg(feature = "json-escape-safe")]
1386    #[test]
1387    fn escape_cropping_never_emits_a_partial_escape() {
1388        let payload = r#""a\"b\\c\nd\u0001eeee""#;
1389        for width in 1..=24 {
1390            let output = Console::builder()
1391                .width(width)
1392                .force_terminal(false)
1393                .build()
1394                .render_to_string(&Json::new(payload).unwrap().no_wrap(true).escape_safe(true));
1395            let mut chars = output.chars();
1396            while let Some(c) = chars.next() {
1397                if c == '\\' {
1398                    let next = chars.next().expect("complete short escape");
1399                    if next == 'u' {
1400                        for _ in 0..4 {
1401                            assert!(chars.next().is_some_and(|c| c.is_ascii_hexdigit()));
1402                        }
1403                    }
1404                }
1405            }
1406        }
1407    }
1408
1409    /// Nested inside another renderable, `JSON.text.no_wrap` survives and each
1410    /// line is **cropped** at the width rather than wrapped — see
1411    /// [`Json::no_wrap`]. Wrapping here instead was silent content loss: with
1412    /// `rich -j doc.json -w 120` on an 80-column console the document was laid
1413    /// out at 120 and then cropped to 80 by `Console.print`, so whole runs
1414    /// vanished and the surviving text read as if it were contiguous.
1415    ///
1416    /// Captured from rich-cli 1.8.1 driven by rich 15.0.0:
1417    /// `COLUMNS=80 rich -j long.json -w 40`.
1418    #[test]
1419    fn a_nested_document_is_cropped_rather_than_wrapped() {
1420        let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
1421        let console = Console::builder().width(40).color_system(None).build();
1422        let json = Json::new(payload).expect("valid json").no_wrap(true);
1423        assert_eq!(
1424            console.render_to_string(&json),
1425            "{\n  \"k\": \"the quick brown fox jumps over t\n}"
1426        );
1427
1428        // …and the wrap is still the default, because a bare
1429        // `Console.print(JSON(...))` loses the flag in `Text.join`.
1430        assert_eq!(
1431            render_plain(payload, 40),
1432            "{\n  \"k\": \"the quick brown fox jumps over \n\
1433             the lazy dog and keeps running for a \n\
1434             very long time indeed\"\n}"
1435        );
1436    }
1437
1438    /// A crop must not cut a double-width character in half: `set_cell_size`
1439    /// drops the straddling character and the line comes out one cell short,
1440    /// never one cell over.
1441    #[test]
1442    fn cropping_never_splits_a_wide_character() {
1443        let console = Console::builder().width(12).color_system(None).build();
1444        let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
1445            .expect("valid json")
1446            .no_wrap(true);
1447        for line in console.render_to_string(&json).lines() {
1448            assert!(
1449                crate::cells::cell_len(line) <= 12,
1450                "line {line:?} overflows the crop"
1451            );
1452        }
1453    }
1454
1455    /// serde_json's default float parser takes a fast path that can land 1 ULP
1456    /// from the value in the file, so the rendered number parsed back to a
1457    /// *different* double. The `float_roundtrip` feature makes parsing exact.
1458    #[test]
1459    fn floats_round_trip_exactly() {
1460        for literal in [
1461            "-938371.9565467801",
1462            "0.1",
1463            "1.7976931348623157e308",
1464            "5e-324",
1465            "3.141592653589793",
1466        ] {
1467            let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
1468            let rendered: String = out
1469                .split(':')
1470                .nth(1)
1471                .expect("a value after the key")
1472                .trim()
1473                .trim_end_matches(['}', ' ', '\n'])
1474                .to_string();
1475            let want: f64 = literal.parse().expect("literal parses");
1476            let got: f64 = rendered
1477                .parse()
1478                .unwrap_or_else(|_| panic!("rendered {rendered:?}"));
1479            assert_eq!(
1480                got.to_bits(),
1481                want.to_bits(),
1482                "{literal} rendered as {rendered} — a different double"
1483            );
1484        }
1485    }
1486
1487    /// serde_json stops at 128 levels, so a 200-deep document — which CPython
1488    /// parses without complaint — was reported as invalid JSON and the CLI
1489    /// exited 1 on a file upstream renders.
1490    #[test]
1491    fn deep_nesting_is_not_rejected() {
1492        for depth in [128, 129, 200, 1000] {
1493            let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
1494            let json = Json::new(&payload)
1495                .unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
1496            let console = Console::builder()
1497                .width(4 * depth + 8)
1498                .color_system(None)
1499                .build();
1500            let out = console.render_to_string(&json);
1501            assert_eq!(
1502                out.matches('[').count(),
1503                depth,
1504                "depth {depth} did not render every level"
1505            );
1506        }
1507    }
1508
1509    /// Parsing and dropping must cost heap, not stack: a recursive parser (or
1510    /// the compiler's recursive drop glue) turns a deep document into a stack
1511    /// overflow, which kills the process without even an error message.
1512    #[test]
1513    fn very_deep_nesting_does_not_overflow_the_stack() {
1514        let payload = format!("{}1{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
1515        let json = Json::new(&payload).expect("deep document parses");
1516        drop(json);
1517        // Drop glue on a tree far past the parse limit stays iterative too.
1518        let mut node = Node::Number("1".to_string());
1519        for _ in 0..100_000 {
1520            node = Node::Array(vec![node]);
1521        }
1522        drop(node);
1523    }
1524
1525    /// Past `MAX_DEPTH` the document is refused with CPython's
1526    /// `RecursionError` message instead of being rendered: its indentation
1527    /// alone is quadratic in the depth, and a 100 000-deep array exhausted
1528    /// memory and was killed.
1529    #[test]
1530    fn nesting_past_the_limit_is_an_error() {
1531        for (open, close, what) in [("[", "]", "array"), ("{\"a\":", "}", "object")] {
1532            for depth in [MAX_DEPTH + 1, 100_000] {
1533                let payload = format!("{}1{}", open.repeat(depth), close.repeat(depth));
1534                let error = Json::new(&payload).err().expect("too deep");
1535                assert!(
1536                    error.to_string().contains(&format!(
1537                        "maximum recursion depth exceeded while decoding a JSON {what}"
1538                    )),
1539                    "{error}"
1540                );
1541            }
1542        }
1543        // An empty container counts as a level, as a call in CPython's scanner.
1544        let payload = format!("{}[]{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
1545        assert!(Json::new(&payload).is_err());
1546        let payload = format!(
1547            "{}[]{}",
1548            "[".repeat(MAX_DEPTH - 1),
1549            "]".repeat(MAX_DEPTH - 1)
1550        );
1551        assert!(Json::new(&payload).is_ok());
1552    }
1553
1554    /// Python's json accepts and emits `NaN` / `Infinity` / `-Infinity`
1555    /// (`allow_nan=True` is the default), and rich's JSONHighlighter has no
1556    /// rule that matches them, so upstream prints them *unstyled*. serde_json
1557    /// rejected the whole document.
1558    ///
1559    /// Captured from rich 15.0.0:
1560    /// `Console(width=40, force_terminal=True).print(JSON(...))`.
1561    #[test]
1562    fn non_finite_numbers_render_unstyled() {
1563        assert_eq!(
1564            render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
1565            "\x1b[1m{\x1b[0m\n  \x1b[1;34m\"a\"\x1b[0m: NaN,\n  \x1b[1;34m\"b\"\x1b[0m: \
1566             Infinity,\n  \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n  \x1b[1;34m\"d\"\x1b[0m: \
1567             \x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
1568        );
1569        // Python spells them with those exact capitalisations and nothing else.
1570        for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
1571            assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
1572        }
1573    }
1574
1575    /// The hand-written reader must accept and reject exactly what serde_json
1576    /// does for bounded numbers — Python also accepts overflowing exponents.
1577    #[test]
1578    fn acceptance_matches_serde_json() {
1579        let samples = [
1580            "{}",
1581            "[]",
1582            "  {\t\"a\" :\n1 }  ",
1583            r#"{"a": 1, "a": 2}"#,
1584            r#"{"a": [1, {"b": null}], "c": "x"}"#,
1585            "0",
1586            "-0",
1587            "0.0",
1588            "1e10",
1589            "1E+10",
1590            "1e-7",
1591            "12345678901234567890",
1592            "-12345678901234567890123456789012345",
1593            "01",
1594            "1.",
1595            ".1",
1596            "+1",
1597            "1e",
1598            "-",
1599            "--1",
1600            "-i",
1601            "-Inf",
1602            "Infinit",
1603            "NAN",
1604            "1 2",
1605            "",
1606            "   ",
1607            "{",
1608            "[",
1609            "]",
1610            "}",
1611            "[,]",
1612            "[1,]",
1613            r#"{"a": 1,}"#,
1614            r#"{a: 1}"#,
1615            r#"{'a': 1}"#,
1616            r#"{"a" 1}"#,
1617            "[1 2]",
1618            "truex",
1619            "tru",
1620            "nul",
1621            r#""unterminated"#,
1622            r#""\q""#,
1623            r#""é""#,
1624            r#""😀""#,
1625            "\"raw\nnewline\"",
1626            r#""café ❤""#,
1627            "\"\u{e9}\\\"",
1628            "\u{feff}{}",
1629            "[[[[1]]]]",
1630        ];
1631        for sample in samples {
1632            let ours = Json::new(sample).is_ok();
1633            let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
1634            assert_eq!(ours, theirs, "disagreed about {sample:?}");
1635        }
1636    }
1637
1638    #[test]
1639    fn python_numbers_preserve_large_integers_and_overflow() {
1640        for number in [
1641            "1234567890123456789012345678901234567890",
1642            "-1234567890123456789012345678901234567890",
1643        ] {
1644            assert_eq!(render_plain(number, 100), number);
1645        }
1646        assert_eq!(render_plain("-0", 100), "0");
1647        assert_eq!(render_plain("1e400", 100), "Infinity");
1648        assert_eq!(render_plain("-1e999", 100), "-Infinity");
1649        for invalid in ["01", "-01", "1.e2", "1e+", "1e400x", "--1", "1+2", ".1"] {
1650            assert!(Json::new(invalid).is_err(), "accepted {invalid}");
1651        }
1652    }
1653
1654    /// And the tree it builds must be the tree serde_json would have built.
1655    /// `serde_json::to_string_pretty` happens to use the very layout upstream's
1656    /// `json.dumps(indent=2)` does, so it doubles as a reference dump: key
1657    /// order, repeated-key collapsing, string escaping and number formatting
1658    /// all have to agree.
1659    #[test]
1660    fn the_parsed_tree_matches_serde_json() {
1661        let samples = [
1662            r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
1663            r#"{"a": 1, "b": 2, "a": 3}"#,
1664            r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
1665            r#"{"k": "A\t\"x\"A\\\/é"}"#,
1666            r#"[0, 12345678901234567890, -7]"#,
1667            r#"{"café": "❤", "": ""}"#,
1668            "[]",
1669            "{}",
1670            "\"top level\"",
1671            "1234",
1672        ];
1673        for sample in samples {
1674            let reference: serde_json::Value =
1675                serde_json::from_str(sample).expect("sample is valid JSON");
1676            assert_eq!(
1677                render_plain(sample, 10_000),
1678                serde_json::to_string_pretty(&reference).expect("value re-serialises"),
1679                "diverged on {sample}"
1680            );
1681        }
1682    }
1683
1684    /// Floats are written with Python's `float.__repr__`, as `json.dumps` does,
1685    /// not serde_json's spelling (`1e-7`, `10000000000.0`).
1686    #[test]
1687    fn floats_render_as_python_repr() {
1688        let out = render_plain(
1689            "[-0.5, 1e10, 1E+10, 1e-7, 1.7976931348623157e308, 1e16]",
1690            10_000,
1691        );
1692        let values: Vec<&str> = out
1693            .lines()
1694            .filter_map(|line| line.trim().strip_suffix(',').or(Some(line.trim())))
1695            .filter(|value| !matches!(*value, "[" | "]"))
1696            .collect();
1697        assert_eq!(
1698            values,
1699            [
1700                "-0.5",
1701                "10000000000.0",
1702                "10000000000.0",
1703                "1e-07",
1704                "1.7976931348623157e+308",
1705                "1e+16"
1706            ]
1707        );
1708    }
1709
1710    /// A repeated key collapses to one entry — first position, last value —
1711    /// which is what both `dict` and serde_json's `preserve_order` produce.
1712    #[test]
1713    fn a_repeated_key_keeps_its_position_and_last_value() {
1714        assert_eq!(
1715            render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
1716            "{\n  \"a\": 3,\n  \"b\": 2\n}"
1717        );
1718    }
1719
1720    /// Escapes are decoded and re-encoded, because upstream re-serialises the
1721    /// parsed data with `json.dumps`.
1722    #[test]
1723    fn escapes_are_re_encoded_like_dumps() {
1724        assert_eq!(
1725            render_plain(r#"{"k": "A\t\"x\""}"#, 60),
1726            "{\n  \"k\": \"A\\t\\\"x\\\"\"\n}"
1727        );
1728    }
1729}