use crate::json::DecodeError;
use crate::parser::{InnerParser, Parser, Peek};
use crate::{JsonError, JsonParserConfig};
#[derive(Clone, Copy)]
pub struct PrettifyConfig<'a> {
pub indent: &'a str,
pub max_line_width: usize,
pub max_inline_depth: usize,
pub max_inline_object_entries: usize,
pub max_inline_array_entries: usize,
pub inline_bracket_padding: bool,
pub parser: JsonParserConfig,
}
impl<'a> PrettifyConfig<'a> {
pub const SMART: PrettifyConfig<'static> = PrettifyConfig {
indent: " ",
max_line_width: 90,
max_inline_depth: 2,
max_inline_object_entries: 1,
max_inline_array_entries: 8,
inline_bracket_padding: false,
parser: JsonParserConfig {
recursion_limit: 128,
allow_trailing_commas: false,
allow_comments: false,
allow_unquoted_field_keys: false,
allow_trailing_data: false,
},
};
}
impl Default for PrettifyConfig<'_> {
fn default() -> Self {
Self {
indent: " ",
max_line_width: 0,
max_inline_depth: usize::MAX,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: false,
parser: JsonParserConfig::default(),
}
}
}
static TRAILING_CHARACTERS: DecodeError = DecodeError {
message: "Trailing characters",
};
pub fn prettify(json: &str, config: &PrettifyConfig<'_>) -> Result<String, JsonError> {
let mut parser = Parser::new(json, config.parser);
let mut out = String::with_capacity(json.len() + json.len() / 8);
match walk(&mut parser, json, &mut out, config) {
Ok(()) => {
if config.parser.allow_trailing_data || parser.at.eat_whitespace().is_none() {
Ok(out)
} else {
Err(JsonError::new(&TRAILING_CHARACTERS, None))
}
}
Err(err) => Err(JsonError::extract(err, &mut parser)),
}
}
fn write_newline_indent(out: &mut String, indent: &str, depth: usize) {
out.push('\n');
for _ in 0..depth {
out.push_str(indent);
}
}
fn copy_scalar_verbatim(
p: &mut InnerParser<'_>,
json: &str,
out: &mut String,
) -> Result<(), &'static DecodeError> {
let start = p.index;
match p.skip_value() {
Ok(()) => {}
Err(e) => return Err(e),
}
out.push_str(&json[start..p.index]);
Ok(())
}
fn emit_key(
p: &mut InnerParser<'_>,
json: &str,
out: &mut String,
) -> Result<(), &'static DecodeError> {
match copy_scalar_verbatim(p, json, out) {
Ok(()) => {}
Err(e) => return Err(e),
}
match p.discard_colon() {
Ok(()) => {}
Err(e) => return Err(e),
}
out.push_str(": ");
Ok(())
}
fn flat_scalar(p: &mut InnerParser<'_>, json: &str, out: &mut String, limit: usize) -> bool {
let start = p.index;
if p.skip_value().is_err() {
return false;
}
let end = p.index;
if out.len() + (end - start) > limit {
return false;
}
out.push_str(&json[start..end]);
true
}
fn flat_value(
p: &mut InnerParser<'_>,
json: &str,
out: &mut String,
limit: usize,
depth: usize,
config: &PrettifyConfig<'_>,
peek: Peek,
) -> bool {
let max_obj = config.max_inline_object_entries;
let max_arr = config.max_inline_array_entries;
let pad = if config.inline_bracket_padding {
" "
} else {
""
};
match peek {
Peek::Array => {
if depth == 0 {
return false;
}
out.push('[');
match p.enter_seen_array() {
Ok(Some(mut elem)) => {
if max_arr == 0 {
return false;
}
out.push_str(pad);
let mut elems = 1;
loop {
if !flat_value(p, json, out, limit, depth - 1, config, elem) {
return false;
}
match p.array_step() {
Ok(Some(next)) => {
elems += 1;
if elems > max_arr {
return false;
}
out.push_str(", ");
if out.len() > limit {
return false;
}
elem = next;
}
Ok(None) => {
out.push_str(pad);
out.push(']');
return out.len() <= limit;
}
Err(_) => return false,
}
}
}
Ok(None) => {
out.push(']');
out.len() <= limit
}
Err(_) => false,
}
}
Peek::Object => {
if depth == 0 {
return false;
}
out.push('{');
match p.enter_seen_object_at_first_key() {
Ok(Some(())) => {
if max_obj == 0 {
return false;
}
out.push_str(pad);
let mut keys = 1;
loop {
if !flat_scalar(p, json, out, limit) {
return false;
}
if p.discard_colon().is_err() {
return false;
}
out.push_str(": ");
if out.len() > limit {
return false;
}
let value = match p.peek() {
Ok(value) => value,
Err(_) => return false,
};
if !flat_value(p, json, out, limit, depth - 1, config, value) {
return false;
}
match p.object_step_at_key() {
Ok(Some(())) => {
keys += 1;
if keys > max_obj {
return false;
}
out.push_str(", ");
if out.len() > limit {
return false;
}
}
Ok(None) => {
out.push_str(pad);
out.push('}');
return out.len() <= limit;
}
Err(_) => return false,
}
}
}
Ok(None) => {
out.push('}');
out.len() <= limit
}
Err(_) => false,
}
}
_ => flat_scalar(p, json, out, limit),
}
}
#[derive(Clone, Copy)]
enum Frame {
Array,
Object,
}
fn walk(
parser: &mut Parser<'_>,
json: &str,
out: &mut String,
config: &PrettifyConfig<'_>,
) -> Result<(), &'static DecodeError> {
let indent = config.indent;
let max_line_width = config.max_line_width;
let max_inline_depth = config.max_inline_depth;
let mut stack: Vec<Frame> = Vec::with_capacity(16);
let mut peek = match parser.at.peek() {
Ok(p) => p,
Err(e) => return Err(e),
};
'outer: loop {
let budget = max_line_width.saturating_sub(stack.len() * indent.len());
let inlined =
if budget > 0 && max_inline_depth > 0 && matches!(peek, Peek::Array | Peek::Object) {
let start = out.len();
let snap_index = parser.at.index;
let snap_limit = parser.at.config.recursion_limit;
if flat_value(
&mut parser.at,
json,
out,
start + budget,
max_inline_depth,
config,
peek,
) {
true
} else {
out.truncate(start);
parser.at.index = snap_index;
parser.at.config.recursion_limit = snap_limit;
false
}
} else {
false
};
if !inlined {
match peek {
Peek::Array => {
out.push('[');
match parser.at.enter_seen_array() {
Ok(Some(p)) => {
stack.push(Frame::Array);
write_newline_indent(out, indent, stack.len());
peek = p;
continue 'outer;
}
Ok(None) => {
out.push(']');
}
Err(e) => return Err(e),
}
}
Peek::Object => {
out.push('{');
match parser.at.enter_seen_object_at_first_key() {
Ok(Some(())) => {
stack.push(Frame::Object);
write_newline_indent(out, indent, stack.len());
match emit_key(&mut parser.at, json, out) {
Ok(()) => {}
Err(e) => return Err(e),
}
peek = match parser.at.peek() {
Ok(p) => p,
Err(e) => return Err(e),
};
continue 'outer;
}
Ok(None) => {
out.push('}');
}
Err(e) => return Err(e),
}
}
_ => match copy_scalar_verbatim(&mut parser.at, json, out) {
Ok(()) => {}
Err(e) => return Err(e),
},
}
}
loop {
let top = match stack.last() {
Some(&t) => t,
None => return Ok(()),
};
match top {
Frame::Object => match parser.at.object_step_at_key() {
Ok(Some(())) => {
out.push(',');
write_newline_indent(out, indent, stack.len());
match emit_key(&mut parser.at, json, out) {
Ok(()) => {}
Err(e) => return Err(e),
}
peek = match parser.at.peek() {
Ok(p) => p,
Err(e) => return Err(e),
};
continue 'outer;
}
Ok(None) => {
stack.pop();
write_newline_indent(out, indent, stack.len());
out.push('}');
}
Err(e) => return Err(e),
},
Frame::Array => match parser.at.array_step() {
Ok(Some(p)) => {
out.push(',');
write_newline_indent(out, indent, stack.len());
peek = p;
continue 'outer;
}
Ok(None) => {
stack.pop();
write_newline_indent(out, indent, stack.len());
out.push(']');
}
Err(e) => return Err(e),
},
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pretty(json: &str) -> String {
prettify(json, &PrettifyConfig::default()).unwrap()
}
#[test]
fn empty_containers() {
assert_eq!(pretty("[]"), "[]");
assert_eq!(pretty("{}"), "{}");
assert_eq!(pretty(" [] "), "[]");
}
#[test]
fn scalars() {
assert_eq!(pretty("null"), "null");
assert_eq!(pretty("true"), "true");
assert_eq!(pretty("false"), "false");
assert_eq!(pretty("42"), "42");
assert_eq!(pretty("-3.14e2"), "-3.14e2");
assert_eq!(pretty(r#""hello""#), r#""hello""#);
}
#[test]
fn nested_mixed() {
let input = r#"{"a":[1,2,{"b":null}],"c":"x\n"}"#;
let expected = "{\n \"a\": [\n 1,\n 2,\n {\n \"b\": null\n }\n ],\n \"c\": \"x\\n\"\n}";
assert_eq!(pretty(input), expected);
}
#[test]
fn strings_preserve_escapes_verbatim() {
let input = r#"["a\nb","ÿ","\\","\""]"#;
let expected = "[\n \"a\\nb\",\n \"ÿ\",\n \"\\\\\",\n \"\\\"\"\n]";
assert_eq!(pretty(input), expected);
let input_u = "[\"\\u00ff\"]";
let expected_u = "[\n \"\\u00ff\"\n]";
assert_eq!(pretty(input_u), expected_u);
}
#[test]
fn numbers_verbatim() {
let input = r#"[0,1,-1,1.5,1e10,1.5e-3,1E+2]"#;
let expected = "[\n 0,\n 1,\n -1,\n 1.5,\n 1e10,\n 1.5e-3,\n 1E+2\n]";
assert_eq!(pretty(input), expected);
}
#[test]
fn custom_indent() {
let cfg = PrettifyConfig {
indent: "\t",
max_line_width: 0,
max_inline_depth: usize::MAX,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: false,
parser: JsonParserConfig::default(),
};
let out = prettify(r#"{"a":1}"#, &cfg).unwrap();
assert_eq!(out, "{\n\t\"a\": 1\n}");
}
#[test]
fn nested_empty_containers_inline() {
assert_eq!(pretty(r#"[[],{}]"#), "[\n [],\n {}\n]");
}
#[test]
fn malformed_returns_error() {
assert!(prettify("{", &PrettifyConfig::default()).is_err());
assert!(prettify("[1,]", &PrettifyConfig::default()).is_err());
assert!(prettify("not json", &PrettifyConfig::default()).is_err());
}
#[test]
fn trailing_data_rejected_by_default() {
assert!(prettify("1 2", &PrettifyConfig::default()).is_err());
}
#[test]
fn trailing_data_allowed_with_flag() {
let cfg = PrettifyConfig {
indent: " ",
max_line_width: 0,
max_inline_depth: usize::MAX,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: false,
parser: JsonParserConfig {
allow_trailing_data: true,
..JsonParserConfig::default()
},
};
assert_eq!(prettify("1 garbage", &cfg).unwrap(), "1");
}
#[test]
fn trailing_commas_lenient() {
let cfg = PrettifyConfig {
indent: " ",
max_line_width: 0,
max_inline_depth: usize::MAX,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: false,
parser: JsonParserConfig {
allow_trailing_commas: true,
..JsonParserConfig::default()
},
};
assert_eq!(prettify("[1,2,]", &cfg).unwrap(), "[\n 1,\n 2\n]");
}
#[test]
fn deep_nesting_is_iterative() {
let deep = format!("{}1{}", "[".repeat(100_000), "]".repeat(100_000));
assert!(prettify(&deep, &PrettifyConfig::default()).is_err());
let shallow = format!("{}1{}", "[".repeat(100), "]".repeat(100));
assert!(prettify(&shallow, &PrettifyConfig::default()).is_ok());
}
#[test]
fn unquoted_keys_unsupported() {
let cfg = PrettifyConfig {
indent: " ",
max_line_width: 0,
max_inline_depth: usize::MAX,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: false,
parser: JsonParserConfig {
allow_unquoted_field_keys: true,
..JsonParserConfig::default()
},
};
assert!(prettify("{a:1}", &cfg).is_err());
}
fn pretty_width(json: &str, max_line_width: usize) -> String {
pretty_full(json, max_line_width, usize::MAX)
}
fn pretty_caps(
json: &str,
max_inline_object_entries: usize,
max_inline_array_entries: usize,
) -> String {
let cfg = PrettifyConfig {
indent: " ",
max_line_width: 80,
max_inline_depth: usize::MAX,
max_inline_object_entries,
max_inline_array_entries,
inline_bracket_padding: false,
parser: JsonParserConfig::default(),
};
prettify(json, &cfg).unwrap()
}
fn pretty_pad(json: &str) -> String {
let cfg = PrettifyConfig {
indent: " ",
max_line_width: 80,
max_inline_depth: usize::MAX,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: true,
parser: JsonParserConfig::default(),
};
prettify(json, &cfg).unwrap()
}
fn pretty_obj(json: &str, max_inline_object_entries: usize) -> String {
pretty_caps(json, max_inline_object_entries, usize::MAX)
}
fn pretty_arr(json: &str, max_inline_array_entries: usize) -> String {
pretty_caps(json, usize::MAX, max_inline_array_entries)
}
#[test]
fn inline_object_entries_cap() {
let input = r#"{"a":1,"b":2}"#;
assert_eq!(pretty_obj(input, 2), r#"{"a": 1, "b": 2}"#);
assert_eq!(pretty_obj(input, 1), "{\n \"a\": 1,\n \"b\": 2\n}");
assert_eq!(pretty_obj(r#"{"a":1}"#, 0), "{\n \"a\": 1\n}");
assert_eq!(pretty_obj("{}", 0), "{}");
}
#[test]
fn inline_object_entries_cap_propagates() {
let input = r#"[{"a":1,"b":2},{"c":3}]"#;
assert_eq!(
pretty_obj(input, 1),
"[\n {\n \"a\": 1,\n \"b\": 2\n },\n {\"c\": 3}\n]"
);
}
#[test]
fn inline_array_entries_cap() {
let input = r#"[1,2,3]"#;
assert_eq!(pretty_arr(input, 3), "[1, 2, 3]");
assert_eq!(pretty_arr(input, 2), "[\n 1,\n 2,\n 3\n]");
assert_eq!(pretty_arr(r#"[1]"#, 0), "[\n 1\n]");
assert_eq!(pretty_arr("[]", 0), "[]");
}
#[test]
fn inline_array_entries_cap_propagates() {
let input = r#"{"a":[1,2,3],"b":[9]}"#;
assert_eq!(
pretty_arr(input, 2),
"{\n \"a\": [\n 1,\n 2,\n 3\n ],\n \"b\": [9]\n}"
);
}
#[test]
fn inline_caps_are_independent() {
assert_eq!(
pretty_caps(r#"{"a":[1,2,3]}"#, 0, usize::MAX),
"{\n \"a\": [1, 2, 3]\n}"
);
assert_eq!(
pretty_caps(r#"[{"a":1},{"b":2}]"#, usize::MAX, 0),
"[\n {\"a\": 1},\n {\"b\": 2}\n]"
);
}
#[test]
fn inline_bracket_padding_pads_non_empty() {
assert_eq!(pretty_pad(r#"{"a":[1,2]}"#), r#"{ "a": [ 1, 2 ] }"#);
}
#[test]
fn inline_bracket_padding_skips_empty() {
assert_eq!(pretty_pad(r#"[{},[]]"#), "[ {}, [] ]");
assert_eq!(pretty_pad("{}"), "{}");
assert_eq!(pretty_pad("[]"), "[]");
}
fn pretty_full(json: &str, max_line_width: usize, max_inline_depth: usize) -> String {
let cfg = PrettifyConfig {
indent: " ",
max_line_width,
max_inline_depth,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: false,
parser: JsonParserConfig::default(),
};
prettify(json, &cfg).unwrap()
}
#[test]
fn inline_small_array() {
assert_eq!(
pretty_width(r#"{"a":[[0,1],[1,0]]}"#, 20),
"{\n \"a\": [[0, 1], [1, 0]]\n}"
);
}
#[test]
fn inline_whole_document() {
assert_eq!(
pretty_width(r#"{"a":[[0,1],[1,0]]}"#, 80),
"{\"a\": [[0, 1], [1, 0]]}"
);
}
#[test]
fn width_zero_is_always_expand() {
let input = r#"{"a":[1,2,3]}"#;
assert_eq!(pretty_width(input, 0), pretty(input));
}
#[test]
fn depth_tightens_budget() {
let shallow = pretty_width(r#"[[1,2],0,0,0,0,0]"#, 8);
assert!(shallow.contains("[1, 2]"), "{shallow}");
let deep = pretty_width(r#"[[[1,2],0,0,0,0,0]]"#, 8);
assert!(!deep.contains("[1, 2]"), "{deep}");
}
#[test]
fn width_boundary() {
assert_eq!(pretty_width(r#"[1,2,3]"#, 9), "[1, 2, 3]");
assert_eq!(pretty_width(r#"[1,2,3]"#, 8), "[\n 1,\n 2,\n 3\n]");
}
#[test]
fn inline_mixed_object() {
assert_eq!(
pretty_width(r#"{"a":[1,2],"b":[100,200,300,400]}"#, 14),
"{\n \"a\": [1, 2],\n \"b\": [\n 100,\n 200,\n 300,\n 400\n ]\n}"
);
}
#[test]
fn inline_depth_caps_nesting() {
let input = r#"{"a":[[0,1],[1,0]]}"#;
assert_eq!(
pretty_full(input, 80, 1),
"{\n \"a\": [\n [0, 1],\n [1, 0]\n ]\n}"
);
assert_eq!(pretty_full(input, 80, 2), "{\n \"a\": [[0, 1], [1, 0]]\n}");
assert_eq!(pretty_full(input, 80, 3), "{\"a\": [[0, 1], [1, 0]]}");
}
#[test]
fn inline_preserves_errors() {
let cfg = PrettifyConfig {
indent: " ",
max_line_width: 80,
max_inline_depth: usize::MAX,
max_inline_object_entries: usize::MAX,
max_inline_array_entries: usize::MAX,
inline_bracket_padding: false,
parser: JsonParserConfig::default(),
};
assert!(prettify("[1,2,]", &cfg).is_err());
assert!(prettify("{\"a\":}", &cfg).is_err());
assert!(prettify("[1,2", &cfg).is_err());
}
}