use std::collections::HashMap;
pub const MAX_DEPTH: usize = 10_000;
use crate::console::{Console, ConsoleOptions};
use crate::errors::{Result, RichError};
use crate::measure::Measurement;
use crate::protocol::Renderable;
use crate::segment::Segment;
use crate::style::StyleType;
use crate::text::{Span, Text};
pub struct Json {
value: Node,
options: JsonOptions,
lines: std::sync::OnceLock<Vec<Text>>,
no_wrap: bool,
#[cfg(feature = "json-escape-safe")]
escape_safe: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JsonOptions {
pub indent: Option<String>,
pub highlight: bool,
pub skip_keys: bool,
pub ensure_ascii: bool,
pub check_circular: bool,
pub allow_nan: bool,
pub sort_keys: bool,
}
impl Default for JsonOptions {
fn default() -> Self {
JsonOptions {
indent: Some(" ".to_string()),
highlight: true,
skip_keys: false,
ensure_ascii: false,
check_circular: true,
allow_nan: true,
sort_keys: false,
}
}
}
impl JsonOptions {
pub fn indent_spaces(indent: i64) -> Option<String> {
Some(" ".repeat(usize::try_from(indent).unwrap_or(0)))
}
}
type PyStr = Vec<u8>;
#[derive(Debug)]
enum Node {
Null,
Bool(bool),
Number(String),
NonFinite(&'static str),
Str(PyStr),
Array(Vec<Node>),
Object(Vec<(PyStr, Node)>),
}
impl Drop for Node {
fn drop(&mut self) {
let mut pending: Vec<Node> = Vec::new();
take_children(self, &mut pending);
while let Some(mut node) = pending.pop() {
take_children(&mut node, &mut pending);
}
}
}
fn take_children(node: &mut Node, out: &mut Vec<Node>) {
match node {
Node::Array(items) => out.append(items),
Node::Object(entries) => out.extend(entries.drain(..).map(|(_, value)| value)),
_ => {}
}
}
impl Json {
pub fn new(text: &str) -> Result<Self> {
Json::with_options(text, &JsonOptions::default())
}
pub fn with_options(text: &str, options: &JsonOptions) -> Result<Self> {
let value = Parser::new(text).parse_document()?;
if !options.allow_nan {
if let Some(literal) = first_non_finite(&value) {
return Err(RichError::Json(format!(
"Out of range float values are not JSON compliant: {}",
match literal {
"NaN" => "nan",
"Infinity" => "inf",
_ => "-inf",
}
)));
}
}
Ok(Json {
value,
options: options.clone(),
lines: std::sync::OnceLock::new(),
no_wrap: false,
#[cfg(feature = "json-escape-safe")]
escape_safe: false,
})
}
fn lines(&self) -> &[Text] {
self.lines.get_or_init(|| {
let dumped = dumps(&self.value, &self.options);
let text = if !self.options.highlight {
Text::new(dumped.json)
} else if dumped.exact {
let mut text = Text::new(dumped.json);
let mut spans = dumped.spans;
spans.extend(dumped.keys);
text.set_spans(spans);
text
} else {
json_highlight(dumped.json)
};
if text.plain().contains('\n') {
text.split("\n", false, true)
} else {
vec![text]
}
})
}
pub fn text(&self) -> Text {
match self.lines() {
[line] => line.clone(),
lines => Text::new("\n").join(lines),
}
}
#[cfg(feature = "json-escape-safe")]
pub fn escape_safe(mut self, enabled: bool) -> Self {
self.escape_safe = enabled;
self
}
#[must_use]
pub fn no_wrap(mut self, no_wrap: bool) -> Self {
self.no_wrap = no_wrap;
self
}
#[cfg(feature = "json-escape-safe")]
fn render_unwrapped(&self, console: &Console) -> Vec<Segment> {
let mut out = Vec::new();
for (index, line) in self.lines().iter().enumerate() {
if index > 0 {
out.push(Segment::line());
}
out.extend(line.render(console.theme(), console.base_style()));
}
out
}
}
struct Dumped {
json: String,
spans: Vec<Span>,
keys: Vec<Span>,
exact: bool,
}
impl Dumped {
fn styled(&mut self, token: &str, style: &'static str) {
let start = self.json.len();
self.json.push_str(token);
self.spans.push(Span {
start,
end: self.json.len(),
style: StyleType::Name(style.to_string()),
});
}
fn string(&mut self, quoted: &str, key: bool) {
if quoted.as_bytes().get(quoted.len().wrapping_sub(2)) == Some(&b'\\') {
self.exact = false;
}
let start = self.json.len();
self.styled(quoted, "json.str");
if key {
self.keys.push(Span {
start,
end: self.json.len(),
style: StyleType::Name("json.key".to_string()),
});
}
}
}
fn dumps(value: &Node, options: &JsonOptions) -> Dumped {
use std::borrow::Cow;
let indent = options.indent.as_deref();
let item_separator = if indent.is_some() { "," } else { ", " };
let newline_indent = |level: usize| -> Cow<'static, str> {
match indent {
Some(indent) => Cow::Owned(format!("\n{}", indent.repeat(level))),
None => Cow::Borrowed(""),
}
};
let quote_string = |string: &[u8]| -> String { quote_py(string, options.ensure_ascii) };
enum Task<'a> {
Value(&'a Node, usize),
Plain(Cow<'a, str>),
Close(&'static str),
Key(&'a [u8]),
}
let mut dumped = Dumped {
json: String::new(),
spans: Vec::new(),
keys: Vec::new(),
exact: indent.is_none_or(|indent| indent.chars().all(|c| c == ' ' || c == '\t')),
};
let mut stack = vec![Task::Value(value, 0)];
while let Some(task) = stack.pop() {
let (node, level) = match task {
Task::Plain(text) => {
dumped.json.push_str(&text);
continue;
}
Task::Close(brace) => {
dumped.styled(brace, "json.brace");
continue;
}
Task::Key(key) => {
dumped.string("e_string(key), true);
dumped.json.push_str(": ");
continue;
}
Task::Value(node, level) => (node, level),
};
match node {
Node::Null => dumped.styled("null", "json.null"),
Node::Bool(true) => dumped.styled("true", "json.bool_true"),
Node::Bool(false) => dumped.styled("false", "json.bool_false"),
Node::Number(number) => dumped.styled(number, "json.number"),
Node::NonFinite(literal) => dumped.json.push_str(literal),
Node::Str(string) => dumped.string("e_string(string), false),
Node::Array(items) => {
dumped.styled("[", "json.brace");
if items.is_empty() {
dumped.styled("]", "json.brace");
continue;
}
stack.push(Task::Close("]"));
stack.push(Task::Plain(newline_indent(level)));
let last = items.len() - 1;
for (index, item) in items.iter().enumerate().rev() {
if index != last {
stack.push(Task::Plain(Cow::Borrowed(item_separator)));
}
stack.push(Task::Value(item, level + 1));
stack.push(Task::Plain(newline_indent(level + 1)));
}
}
Node::Object(entries) => {
dumped.styled("{", "json.brace");
if entries.is_empty() {
dumped.styled("}", "json.brace");
continue;
}
let mut order: Vec<&(PyStr, Node)> = entries.iter().collect();
if options.sort_keys {
order.sort_by(|a, b| a.0.cmp(&b.0));
}
stack.push(Task::Close("}"));
stack.push(Task::Plain(newline_indent(level)));
let last = order.len() - 1;
for (index, (key, item)) in order.into_iter().enumerate().rev() {
if index != last {
stack.push(Task::Plain(Cow::Borrowed(item_separator)));
}
stack.push(Task::Value(item, level + 1));
stack.push(Task::Key(key));
stack.push(Task::Plain(newline_indent(level + 1)));
}
}
}
}
dumped
}
fn first_non_finite(value: &Node) -> Option<&'static str> {
let mut pending = vec![value];
while let Some(node) = pending.pop() {
match node {
Node::NonFinite(literal) => return Some(literal),
Node::Array(items) => pending.extend(items.iter()),
Node::Object(entries) => pending.extend(entries.iter().map(|(_, value)| value)),
_ => {}
}
}
None
}
fn ascii_escape(quoted: &str) -> String {
if quoted.bytes().all(|b| (b' '..=b'~').contains(&b)) {
return quoted.to_string();
}
let mut out = String::with_capacity(quoted.len() + 16);
for c in quoted.chars() {
if (' '..='~').contains(&c) {
out.push(c);
continue;
}
let mut units = [0u16; 2];
for unit in c.encode_utf16(&mut units) {
out.push_str(&format!("\\u{unit:04x}"));
}
}
out
}
fn json_patterns() -> &'static (fancy_regex::Regex, fancy_regex::Regex) {
static PATTERNS: std::sync::OnceLock<(fancy_regex::Regex, fancy_regex::Regex)> =
std::sync::OnceLock::new();
PATTERNS.get_or_init(|| {
const JSON_STR: &str = r#"(?<![\\\w])(?P<str>b?".*?(?<!\\)")"#;
let combined = [
r"(?P<brace>[\{\[\(\)\]\}])",
r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b",
r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
JSON_STR,
]
.join("|");
(
fancy_regex::Regex::new(&combined).expect("valid JSON highlighter pattern"),
fancy_regex::Regex::new(JSON_STR).expect("valid JSON string pattern"),
)
})
}
fn json_highlight(json: String) -> Text {
let (combined, string) = json_patterns();
let mut text = Text::new(json);
text.highlight_with_regex(combined, None, "json.");
let plain = text.plain();
let mut keys = Vec::new();
for found in string.find_iter(plain) {
let Ok(found) = found else { break };
for &byte in &plain.as_bytes()[found.end()..] {
match byte {
b':' => keys.push((found.start(), found.end())),
b' ' | b'\n' | b'\r' | b'\t' => continue,
_ => {}
}
break;
}
}
for (start, end) in keys {
text.push_span(Span {
start,
end,
style: StyleType::Name("json.key".to_string()),
});
}
text
}
impl Renderable for Json {
fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> Measurement {
let mut minimum = 0;
let mut maximum = 0;
for line in self.lines() {
let (line_minimum, line_maximum) = line.measurement();
minimum = minimum.max(line_minimum);
maximum = maximum.max(line_maximum);
}
Measurement::new(minimum, maximum)
}
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
#[cfg(feature = "json-escape-safe")]
if self.escape_safe {
let segments = self.render_unwrapped(console);
return escape_safe_lines(&segments, options.max_width, self.no_wrap);
}
let no_wrap = self.no_wrap || options.no_wrap.unwrap_or(false);
let overflow = options.overflow.unwrap_or_default();
let tab_size = match console.tab_size() {
0 => crate::text::DEFAULT_TAB_SIZE,
tab_size => tab_size,
};
let mut out = Vec::new();
let mut first = true;
for line in self.lines() {
for rendered in line.render_lines_wrapped_tabs(
console.theme(),
console.base_style(),
Some(options.max_width),
options.justify,
overflow,
no_wrap,
tab_size,
) {
if !first {
out.push(Segment::line());
}
first = false;
out.extend(rendered);
}
}
out
}
}
#[cfg(feature = "json-escape-safe")]
fn escape_safe_lines(segments: &[Segment], width: usize, crop: bool) -> Vec<Segment> {
if width == 0 {
return Vec::new();
}
let lines = Segment::split_lines(segments);
let last = lines.len().saturating_sub(1);
let mut out = Vec::new();
for (line_index, line) in lines.into_iter().enumerate() {
let plain: String = line
.iter()
.filter(|s| !s.control)
.map(|s| s.text.as_str())
.collect();
if !plain.contains('\\') {
out.extend(if crop {
Segment::crop_lines(&line, width)
} else {
Segment::fold_lines_words(&line, width)
});
} else {
let (spans, _) = crate::cells::split_graphemes(&plain);
let mut atoms = Vec::new();
let mut index = 0;
while index < spans.len() {
let (start, mut end, mut cells) = spans[index];
if plain.as_bytes()[start] == b'\\' {
let escape_end = start
+ if plain.as_bytes().get(start + 1) == Some(&b'u') {
6
} else {
2
};
while end < escape_end && index + 1 < spans.len() {
index += 1;
end = spans[index].1;
cells += spans[index].2;
}
if !crop && cells > width {
for offset in start..escape_end {
atoms.push((offset, offset + 1, 1));
}
if end > escape_end {
atoms.push((escape_end, end, 0));
}
index += 1;
continue;
}
}
atoms.push((start, end, cells));
index += 1;
}
let mut breaks = Vec::new();
let mut cells = 0;
let mut stop = plain.len();
for (start, _, atom_width) in atoms {
if cells + atom_width > width {
if crop {
stop = start;
break;
}
if cells > 0 {
breaks.push(start);
cells = 0;
}
}
cells += atom_width;
}
let mut position = 0;
let mut next = 0;
for segment in line {
if segment.control {
out.push(segment);
continue;
}
let mut buffer = String::new();
for ch in segment.text.chars() {
if position >= stop {
break;
}
if breaks.get(next) == Some(&position) {
if !buffer.is_empty() {
out.push(Segment::new(
std::mem::take(&mut buffer),
segment.style.clone(),
));
}
out.push(Segment::line());
next += 1;
}
buffer.push(ch);
position += ch.len_utf8();
}
if !buffer.is_empty() {
out.push(Segment::new(buffer, segment.style));
}
}
}
if line_index != last {
out.push(Segment::line());
}
}
out
}
fn quote_py(string: &[u8], ensure_ascii: bool) -> String {
let escape = |run: &str| -> String {
let quoted = quote(run);
if ensure_ascii {
ascii_escape("ed)
} else {
quoted
}
};
if let Ok(run) = std::str::from_utf8(string) {
return escape(run);
}
let mut out = String::from("\"");
let mut rest = string;
while !rest.is_empty() {
let valid = match std::str::from_utf8(rest) {
Ok(_) => rest.len(),
Err(error) => error.valid_up_to(),
};
let (run, tail) = rest.split_at(valid);
let quoted = escape(std::str::from_utf8(run).unwrap_or_default());
out.push_str("ed[1..quoted.len() - 1]);
rest = tail;
if let [0xED, b1 @ 0xA0..=0xBF, b2, tail @ ..] = rest {
let unit = 0xD000 | (u32::from(b1 & 0x3F) << 6) | u32::from(b2 & 0x3F);
if ensure_ascii {
out.push_str(&format!("\\u{unit:04x}"));
} else {
out.push(char::REPLACEMENT_CHARACTER);
}
rest = tail;
} else if !rest.is_empty() {
out.push(char::REPLACEMENT_CHARACTER);
rest = &rest[1..];
}
}
out.push('"');
out
}
fn decode_with_surrogates(token: &str) -> Option<PyStr> {
let inner = token.strip_prefix('"')?.strip_suffix('"')?;
let mut out = Vec::with_capacity(inner.len());
let mut chars = inner.chars().peekable();
let hex4 = |chars: &mut std::iter::Peekable<std::str::Chars<'_>>| -> Option<u32> {
let mut value = 0;
for _ in 0..4 {
value = value * 16 + chars.next()?.to_digit(16)?;
}
Some(value)
};
while let Some(c) = chars.next() {
if c < ' ' {
return None;
}
if c != '\\' {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
continue;
}
let decoded = match chars.next()? {
'"' => '"',
'\\' => '\\',
'/' => '/',
'b' => '\u{8}',
'f' => '\u{c}',
'n' => '\n',
'r' => '\r',
't' => '\t',
'u' => {
let mut unit = hex4(&mut chars)?;
if (0xD800..0xDC00).contains(&unit) {
let mut lookahead = chars.clone();
if lookahead.next() == Some('\\') && lookahead.next() == Some('u') {
if let Some(low @ 0xDC00..=0xDFFF) = hex4(&mut lookahead) {
unit = 0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00);
chars = lookahead;
}
}
}
match char::from_u32(unit) {
Some(c) => c,
None => {
out.push(0xE0 | (unit >> 12) as u8);
out.push(0x80 | ((unit >> 6) & 0x3F) as u8);
out.push(0x80 | (unit & 0x3F) as u8);
continue;
}
}
}
_ => return None,
};
let mut buf = [0u8; 4];
out.extend_from_slice(decoded.encode_utf8(&mut buf).as_bytes());
}
Some(out)
}
fn quote(string: &str) -> String {
serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
}
enum Frame {
Array(Vec<Node>),
Object {
entries: Vec<(PyStr, Node)>,
seen: HashMap<PyStr, usize>,
key: PyStr,
},
}
struct Parser<'a> {
src: &'a str,
bytes: &'a [u8],
pos: usize,
}
impl<'a> Parser<'a> {
fn new(src: &'a str) -> Self {
Parser {
src,
bytes: src.as_bytes(),
pos: 0,
}
}
fn parse_document(&mut self) -> Result<Node> {
let value = self.parse_value()?;
self.skip_whitespace();
if self.pos != self.bytes.len() {
return Err(self.error("trailing characters"));
}
Ok(value)
}
fn parse_value(&mut self) -> Result<Node> {
let mut stack: Vec<Frame> = Vec::new();
let mut node: Node;
'descend: loop {
self.skip_whitespace();
if let Some(kind @ (b'[' | b'{')) = self.peek() {
if stack.len() >= MAX_DEPTH {
let what = if kind == b'[' { "array" } else { "object" };
return Err(self.error(&format!(
"maximum recursion depth exceeded while decoding a JSON {what} \
from a unicode string"
)));
}
}
match self.peek() {
Some(b'[') => {
self.pos += 1;
self.skip_whitespace();
if self.peek() == Some(b']') {
self.pos += 1;
node = Node::Array(Vec::new());
} else {
stack.push(Frame::Array(Vec::new()));
continue 'descend;
}
}
Some(b'{') => {
self.pos += 1;
self.skip_whitespace();
if self.peek() == Some(b'}') {
self.pos += 1;
node = Node::Object(Vec::new());
} else {
let key = self.parse_key()?;
stack.push(Frame::Object {
entries: Vec::new(),
seen: HashMap::new(),
key,
});
continue 'descend;
}
}
_ => node = self.parse_scalar()?,
}
loop {
let Some(frame) = stack.last_mut() else {
return Ok(node);
};
let closing = match frame {
Frame::Array(items) => {
items.push(node);
b']'
}
Frame::Object { entries, seen, key } => {
let key = std::mem::take(key);
match seen.get(&key) {
Some(&at) => entries[at].1 = node,
None => {
seen.insert(key.clone(), entries.len());
entries.push((key, node));
}
}
b'}'
}
};
self.skip_whitespace();
match self.peek() {
Some(b',') => {
self.pos += 1;
if closing == b'}' {
let next_key = self.parse_key()?;
if let Some(Frame::Object { key, .. }) = stack.last_mut() {
*key = next_key;
}
}
continue 'descend;
}
Some(byte) if byte == closing => {
self.pos += 1;
node = match stack.pop() {
Some(Frame::Array(items)) => Node::Array(items),
Some(Frame::Object { entries, .. }) => Node::Object(entries),
None => unreachable!("the frame was just borrowed"),
};
}
_ if closing == b']' => return Err(self.error("expected `,` or `]`")),
_ => return Err(self.error("expected `,` or `}`")),
}
}
}
}
fn parse_key(&mut self) -> Result<PyStr> {
self.skip_whitespace();
if self.peek() != Some(b'"') {
return Err(self.error("key must be a string"));
}
let key = self.parse_string()?;
self.skip_whitespace();
if self.peek() != Some(b':') {
return Err(self.error("expected `:`"));
}
self.pos += 1;
Ok(key)
}
fn parse_scalar(&mut self) -> Result<Node> {
match self.peek() {
Some(b'"') => Ok(Node::Str(self.parse_string()?)),
Some(b't') => {
self.expect_literal("true")?;
Ok(Node::Bool(true))
}
Some(b'f') => {
self.expect_literal("false")?;
Ok(Node::Bool(false))
}
Some(b'n') => {
self.expect_literal("null")?;
Ok(Node::Null)
}
Some(b'N') => {
self.expect_literal("NaN")?;
Ok(Node::NonFinite("NaN"))
}
Some(b'I') => {
self.expect_literal("Infinity")?;
Ok(Node::NonFinite("Infinity"))
}
Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
self.pos += "-Infinity".len();
Ok(Node::NonFinite("-Infinity"))
}
Some(b'-' | b'0'..=b'9') => self.parse_number(),
Some(_) => Err(self.error("expected value")),
None => Err(self.error("EOF while parsing a value")),
}
}
fn parse_string(&mut self) -> Result<PyStr> {
let start = self.pos;
let mut end = self.pos + 1;
loop {
match self.bytes.get(end) {
None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
Some(b'"') => {
end += 1;
break;
}
Some(b'\\') => {
end += 1;
match self.src[end..].chars().next() {
Some(ch) => end += ch.len_utf8(),
None => {
return Err(
self.error_at(self.bytes.len(), "EOF while parsing a string")
)
}
}
}
Some(_) => end += 1,
}
}
let token = &self.src[start..end];
let decoded = match serde_json::from_str::<String>(token) {
Ok(decoded) => decoded.into_bytes(),
Err(error) => decode_with_surrogates(token)
.ok_or_else(|| self.error_at(start, &describe(&error)))?,
};
self.pos = end;
Ok(decoded)
}
fn parse_number(&mut self) -> Result<Node> {
let start = self.pos;
let mut end = start;
while matches!(
self.bytes.get(end),
Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
) {
end += 1;
}
let token = &self.src[start..end];
let digits = token.as_bytes();
let mut i = usize::from(digits.first() == Some(&b'-'));
if digits.get(i) == Some(&b'0') {
i += 1;
} else {
let first = i;
while digits.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == first {
return Err(self.error_at(start, "invalid number"));
}
}
let mut floating = false;
if digits.get(i) == Some(&b'.') {
floating = true;
i += 1;
let first = i;
while digits.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == first {
return Err(self.error_at(start, "invalid number"));
}
}
if matches!(digits.get(i), Some(b'e' | b'E')) {
floating = true;
i += 1;
if matches!(digits.get(i), Some(b'+' | b'-')) {
i += 1;
}
let first = i;
while digits.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == first {
return Err(self.error_at(start, "invalid number"));
}
}
if i != digits.len() {
return Err(self.error_at(start, "invalid number"));
}
self.pos = end;
if !floating {
return Ok(Node::Number(
if token == "-0" { "0" } else { token }.to_string(),
));
}
let value: f64 = token
.parse()
.map_err(|_| self.error_at(start, "invalid number"))?;
if value.is_infinite() {
return Ok(Node::NonFinite(if value.is_sign_negative() {
"-Infinity"
} else {
"Infinity"
}));
}
Ok(Node::Number(crate::pyformat::float_repr(value)))
}
fn expect_literal(&mut self, literal: &str) -> Result<()> {
if self.src[self.pos..].starts_with(literal) {
self.pos += literal.len();
Ok(())
} else {
Err(self.error("expected value"))
}
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn skip_whitespace(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.pos += 1;
}
}
fn error(&self, message: &str) -> RichError {
self.error_at(self.pos, message)
}
fn error_at(&self, pos: usize, message: &str) -> RichError {
let (line, column) = self.line_column(pos);
RichError::Json(format!("{message} at line {line} column {column}"))
}
fn line_column(&self, pos: usize) -> (usize, usize) {
let mut pos = pos.min(self.src.len());
while !self.src.is_char_boundary(pos) {
pos -= 1;
}
let before = &self.src[..pos];
let line = 1 + before.matches('\n').count();
let column = before
.rsplit('\n')
.next()
.map_or(0, |tail| tail.chars().count())
+ 1;
(line, column)
}
}
fn describe(error: &serde_json::Error) -> String {
let text = error.to_string();
match text.find(" at line ") {
Some(at) => text[..at].to_string(),
None => text,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
#[test]
fn recorded_spans_match_the_highlighter_regexes() {
let documents = [
r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
r#"[1e20, -0.0, 1.5e-7, 12345678901234567890, false, [], {}, [[]], {"a": {}}]"#,
r#"{"true": "false", "x:y": "a\"b", "q\"": ": null", "n": [NaN, Infinity, -Infinity]}"#,
r#"{"caf\u00e9": "\u2764 \ud83d\ude00", "ctl": "\u0001\t\n", "": ""}"#,
r#"["[{(1)}]", "0x1F", "b\"x", "\\\"", "tail\\x"]"#,
];
let indents = [None, Some(" "), Some("\t"), Some("")];
for document in documents {
for indent in indents {
for ensure_ascii in [false, true] {
let options = JsonOptions {
indent: indent.map(str::to_string),
ensure_ascii,
sort_keys: true,
..JsonOptions::default()
};
let value = Parser::new(document).parse_document().unwrap();
let dumped = dumps(&value, &options);
assert!(dumped.exact, "{document} / {indent:?}");
let mut recorded = dumped.spans.clone();
recorded.extend(dumped.keys.clone());
let expected = json_highlight(dumped.json.clone());
assert_eq!(recorded, expected.spans(), "{document} / {indent:?}");
}
}
}
let value = Parser::new(r#"{"k": "a\\"}"#).parse_document().unwrap();
assert!(!dumps(&value, &JsonOptions::default()).exact);
let value = Parser::new("[1]").parse_document().unwrap();
let options = JsonOptions {
indent: Some("1".to_string()),
..JsonOptions::default()
};
assert!(!dumps(&value, &options).exact);
}
fn render(text: &str) -> String {
let console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(40)
.build();
console.render_to_string(&Json::new(text).unwrap())
}
fn render_plain(text: &str, width: usize) -> String {
let console = Console::builder().width(width).color_system(None).build();
console.render_to_string(&Json::new(text).expect("valid json"))
}
#[test]
fn empty_collections_stay_inline() {
assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
}
#[test]
fn object_with_scalars() {
assert_eq!(
render(r#"{"ok": false}"#),
"\x1b[1m{\x1b[0m\n \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
);
}
#[test]
fn invalid_json_errors() {
assert!(Json::new("{not json}").is_err());
}
#[test]
fn non_ascii_stays_utf8_in_input_order() {
let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
assert!(out.contains('\u{2764}'), "heart stays UTF-8");
let name_at = out.find("name").expect("name key present");
let emoji_at = out.find("emoji").expect("emoji key present");
assert!(name_at < emoji_at, "keys keep input order");
}
#[test]
fn a_long_value_is_wrapped_rather_than_cropped() {
let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
let out = render_plain(&payload, 40);
assert_eq!(
out.matches('y').count(),
120,
"characters were dropped:
{out}"
);
}
#[test]
fn wrapping_breaks_at_word_boundaries() {
let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
assert_eq!(
render_plain(payload, 40),
"{\n \"k\": \"the quick brown fox jumps over \n\
the lazy dog and keeps running for a \n\
very long time indeed\"\n}"
);
}
#[cfg(feature = "json-escape-safe")]
#[test]
fn adjusted_escape_boundaries_preserve_the_remaining_payload() {
let input = format!(r#"{{"v":"aa\u0001{}"}}"#, "b".repeat(40));
for width in 6..=20 {
let output = Console::builder()
.width(width)
.force_terminal(false)
.build()
.render_to_string(&Json::new(&input).unwrap().escape_safe(true));
assert_eq!(output.matches('b').count(), 40, "width {width}: {output:?}");
}
}
#[cfg(feature = "json-escape-safe")]
#[test]
fn wrapping_keeps_json_escapes_atomic_at_narrow_widths() {
let payload = r#"{"v":"a\"b\\c\nd\u0001e"}"#;
for width in 8..=14 {
let output = Console::builder()
.width(width)
.force_terminal(false)
.build()
.render_to_string(&Json::new(payload).unwrap().escape_safe(true));
for line in output.lines() {
let bytes = line.as_bytes();
let mut index = 0;
while index < bytes.len() {
if bytes[index] != b'\\' {
index += 1;
continue;
}
assert!(
index + 1 < bytes.len(),
"split escape at width {width}: {output:?}"
);
if bytes[index + 1] == b'u' {
assert!(
index + 6 <= bytes.len(),
"split unicode escape at width {width}: {output:?}"
);
index += 6;
} else {
index += 2;
}
}
}
}
}
#[cfg(feature = "json-escape-safe")]
#[test]
fn escape_folding_preserves_bytes_even_below_the_escape_width() {
let payload = r#"{"v":"a\"b\\c\nd\u0001eeeeeeeeeeee"}"#;
let wide = Console::builder()
.width(100)
.force_terminal(false)
.build()
.render_to_string(&Json::new(payload).unwrap().escape_safe(true))
.replace('\n', "");
for width in 1..=20 {
let output = Console::builder()
.width(width)
.force_terminal(false)
.build()
.render_to_string(&Json::new(payload).unwrap().escape_safe(true));
assert_eq!(output.replace('\n', ""), wide, "width {width}");
assert!(output
.lines()
.all(|line| crate::cells::cell_len(line) <= width));
}
}
#[cfg(feature = "json-escape-safe")]
#[test]
fn escape_cropping_never_emits_a_partial_escape() {
let payload = r#""a\"b\\c\nd\u0001eeee""#;
for width in 1..=24 {
let output = Console::builder()
.width(width)
.force_terminal(false)
.build()
.render_to_string(&Json::new(payload).unwrap().no_wrap(true).escape_safe(true));
let mut chars = output.chars();
while let Some(c) = chars.next() {
if c == '\\' {
let next = chars.next().expect("complete short escape");
if next == 'u' {
for _ in 0..4 {
assert!(chars.next().is_some_and(|c| c.is_ascii_hexdigit()));
}
}
}
}
}
}
#[test]
fn a_nested_document_is_cropped_rather_than_wrapped() {
let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
let console = Console::builder().width(40).color_system(None).build();
let json = Json::new(payload).expect("valid json").no_wrap(true);
assert_eq!(
console.render_to_string(&json),
"{\n \"k\": \"the quick brown fox jumps over t\n}"
);
assert_eq!(
render_plain(payload, 40),
"{\n \"k\": \"the quick brown fox jumps over \n\
the lazy dog and keeps running for a \n\
very long time indeed\"\n}"
);
}
#[test]
fn cropping_never_splits_a_wide_character() {
let console = Console::builder().width(12).color_system(None).build();
let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
.expect("valid json")
.no_wrap(true);
for line in console.render_to_string(&json).lines() {
assert!(
crate::cells::cell_len(line) <= 12,
"line {line:?} overflows the crop"
);
}
}
#[test]
fn floats_round_trip_exactly() {
for literal in [
"-938371.9565467801",
"0.1",
"1.7976931348623157e308",
"5e-324",
"3.141592653589793",
] {
let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
let rendered: String = out
.split(':')
.nth(1)
.expect("a value after the key")
.trim()
.trim_end_matches(['}', ' ', '\n'])
.to_string();
let want: f64 = literal.parse().expect("literal parses");
let got: f64 = rendered
.parse()
.unwrap_or_else(|_| panic!("rendered {rendered:?}"));
assert_eq!(
got.to_bits(),
want.to_bits(),
"{literal} rendered as {rendered} — a different double"
);
}
}
#[test]
fn deep_nesting_is_not_rejected() {
for depth in [128, 129, 200, 1000] {
let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
let json = Json::new(&payload)
.unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
let console = Console::builder()
.width(4 * depth + 8)
.color_system(None)
.build();
let out = console.render_to_string(&json);
assert_eq!(
out.matches('[').count(),
depth,
"depth {depth} did not render every level"
);
}
}
#[test]
fn very_deep_nesting_does_not_overflow_the_stack() {
let payload = format!("{}1{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
let json = Json::new(&payload).expect("deep document parses");
drop(json);
let mut node = Node::Number("1".to_string());
for _ in 0..100_000 {
node = Node::Array(vec![node]);
}
drop(node);
}
#[test]
fn nesting_past_the_limit_is_an_error() {
for (open, close, what) in [("[", "]", "array"), ("{\"a\":", "}", "object")] {
for depth in [MAX_DEPTH + 1, 100_000] {
let payload = format!("{}1{}", open.repeat(depth), close.repeat(depth));
let error = Json::new(&payload).err().expect("too deep");
assert!(
error.to_string().contains(&format!(
"maximum recursion depth exceeded while decoding a JSON {what}"
)),
"{error}"
);
}
}
let payload = format!("{}[]{}", "[".repeat(MAX_DEPTH), "]".repeat(MAX_DEPTH));
assert!(Json::new(&payload).is_err());
let payload = format!(
"{}[]{}",
"[".repeat(MAX_DEPTH - 1),
"]".repeat(MAX_DEPTH - 1)
);
assert!(Json::new(&payload).is_ok());
}
#[test]
fn non_finite_numbers_render_unstyled() {
assert_eq!(
render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
"\x1b[1m{\x1b[0m\n \x1b[1;34m\"a\"\x1b[0m: NaN,\n \x1b[1;34m\"b\"\x1b[0m: \
Infinity,\n \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n \x1b[1;34m\"d\"\x1b[0m: \
\x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
);
for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
}
}
#[test]
fn acceptance_matches_serde_json() {
let samples = [
"{}",
"[]",
" {\t\"a\" :\n1 } ",
r#"{"a": 1, "a": 2}"#,
r#"{"a": [1, {"b": null}], "c": "x"}"#,
"0",
"-0",
"0.0",
"1e10",
"1E+10",
"1e-7",
"12345678901234567890",
"-12345678901234567890123456789012345",
"01",
"1.",
".1",
"+1",
"1e",
"-",
"--1",
"-i",
"-Inf",
"Infinit",
"NAN",
"1 2",
"",
" ",
"{",
"[",
"]",
"}",
"[,]",
"[1,]",
r#"{"a": 1,}"#,
r#"{a: 1}"#,
r#"{'a': 1}"#,
r#"{"a" 1}"#,
"[1 2]",
"truex",
"tru",
"nul",
r#""unterminated"#,
r#""\q""#,
r#""é""#,
r#""😀""#,
"\"raw\nnewline\"",
r#""café ❤""#,
"\"\u{e9}\\\"",
"\u{feff}{}",
"[[[[1]]]]",
];
for sample in samples {
let ours = Json::new(sample).is_ok();
let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
assert_eq!(ours, theirs, "disagreed about {sample:?}");
}
}
#[test]
fn python_numbers_preserve_large_integers_and_overflow() {
for number in [
"1234567890123456789012345678901234567890",
"-1234567890123456789012345678901234567890",
] {
assert_eq!(render_plain(number, 100), number);
}
assert_eq!(render_plain("-0", 100), "0");
assert_eq!(render_plain("1e400", 100), "Infinity");
assert_eq!(render_plain("-1e999", 100), "-Infinity");
for invalid in ["01", "-01", "1.e2", "1e+", "1e400x", "--1", "1+2", ".1"] {
assert!(Json::new(invalid).is_err(), "accepted {invalid}");
}
}
#[test]
fn the_parsed_tree_matches_serde_json() {
let samples = [
r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
r#"{"a": 1, "b": 2, "a": 3}"#,
r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
r#"{"k": "A\t\"x\"A\\\/é"}"#,
r#"[0, 12345678901234567890, -7]"#,
r#"{"café": "❤", "": ""}"#,
"[]",
"{}",
"\"top level\"",
"1234",
];
for sample in samples {
let reference: serde_json::Value =
serde_json::from_str(sample).expect("sample is valid JSON");
assert_eq!(
render_plain(sample, 10_000),
serde_json::to_string_pretty(&reference).expect("value re-serialises"),
"diverged on {sample}"
);
}
}
#[test]
fn floats_render_as_python_repr() {
let out = render_plain(
"[-0.5, 1e10, 1E+10, 1e-7, 1.7976931348623157e308, 1e16]",
10_000,
);
let values: Vec<&str> = out
.lines()
.filter_map(|line| line.trim().strip_suffix(',').or(Some(line.trim())))
.filter(|value| !matches!(*value, "[" | "]"))
.collect();
assert_eq!(
values,
[
"-0.5",
"10000000000.0",
"10000000000.0",
"1e-07",
"1.7976931348623157e+308",
"1e+16"
]
);
}
#[test]
fn a_repeated_key_keeps_its_position_and_last_value() {
assert_eq!(
render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
"{\n \"a\": 3,\n \"b\": 2\n}"
);
}
#[test]
fn escapes_are_re_encoded_like_dumps() {
assert_eq!(
render_plain(r#"{"k": "A\t\"x\""}"#, 60),
"{\n \"k\": \"A\\t\\\"x\\\"\"\n}"
);
}
}