use super::syntax::{Kind, Piece};
const MAX_DEPTH: usize = 64;
const INDENT: usize = 2;
pub fn reindent(value: &str) -> Option<Vec<Vec<Piece>>> {
let start = value.find(|c: char| !c.is_whitespace())?;
if !matches!(value.as_bytes()[start], b'{' | b'[') {
return None;
}
let mut parser = Parser {
source: value,
at: start,
lines: Vec::new(),
line: Vec::new(),
depth: 0,
};
parser.value(0)?;
parser.space();
if parser.at != value.len() {
return None;
}
parser.lines.push(parser.line);
Some(parser.lines)
}
struct Parser<'a> {
source: &'a str,
at: usize,
lines: Vec<Vec<Piece>>,
line: Vec<Piece>,
depth: usize,
}
impl Parser<'_> {
fn peek(&self) -> Option<u8> {
self.source.as_bytes().get(self.at).copied()
}
fn space(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.at += 1;
}
}
fn take(&mut self, byte: u8) -> Option<()> {
(self.peek()? == byte).then(|| self.at += 1)
}
fn push(&mut self, kind: Kind, text: impl Into<String>) {
self.line.push(Piece::new(kind, text));
}
fn newline(&mut self, indent: usize) {
self.lines.push(std::mem::take(&mut self.line));
if indent > 0 {
self.push(Kind::Punct, " ".repeat(indent * INDENT));
}
}
fn value(&mut self, indent: usize) -> Option<()> {
if self.depth >= MAX_DEPTH {
return None;
}
match self.peek()? {
b'{' => self.container(indent, true),
b'[' => self.container(indent, false),
b'"' => {
let text = self.string()?;
self.push(Kind::Str, text);
Some(())
}
b't' => self.literal("true"),
b'f' => self.literal("false"),
b'n' => self.literal("null"),
b'-' | b'0'..=b'9' => self.number(),
_ => None,
}
}
fn container(&mut self, indent: usize, object: bool) -> Option<()> {
let (open, close) = if object { (b'{', b'}') } else { (b'[', b']') };
self.take(open)?;
self.space();
if self.peek()? == close {
self.at += 1;
self.push(Kind::Punct, if object { "{}" } else { "[]" });
return Some(());
}
self.push(Kind::Punct, if object { "{" } else { "[" });
self.depth += 1;
loop {
self.newline(indent + 1);
self.space();
if object {
let key = self.string()?;
self.push(Kind::Name, key);
self.space();
self.take(b':')?;
self.push(Kind::Punct, ": ");
self.space();
}
self.value(indent + 1)?;
self.space();
match self.peek()? {
b',' => {
self.at += 1;
self.push(Kind::Punct, ",");
}
byte if byte == close => {
self.at += 1;
self.newline(indent);
self.push(Kind::Punct, if object { "}" } else { "]" });
self.depth -= 1;
return Some(());
}
_ => return None,
}
}
}
fn string(&mut self) -> Option<String> {
let start = self.at;
self.take(b'"')?;
loop {
match self.peek()? {
b'\\' => {
self.at += 1;
self.peek()?;
self.at += 1;
}
b'"' => {
self.at += 1;
return Some(self.source[start..self.at].to_string());
}
byte if byte < 0x20 => return None,
_ => {
self.at += 1;
}
}
}
}
fn literal(&mut self, word: &'static str) -> Option<()> {
if self.source[self.at..].starts_with(word) {
self.at += word.len();
self.push(Kind::Lit, word);
return Some(());
}
None
}
fn number(&mut self) -> Option<()> {
let start = self.at;
self.take(b'-');
let whole = self.at;
let digits = self.digits();
if digits == 0 {
return None;
}
if digits > 1 && self.source.as_bytes()[whole] == b'0' {
return None;
}
if self.take(b'.').is_some() && self.digits() == 0 {
return None;
}
if matches!(self.peek(), Some(b'e' | b'E')) {
self.at += 1;
if self.peek() == Some(b'+') || self.peek() == Some(b'-') {
self.at += 1;
}
if self.digits() == 0 {
return None;
}
}
let text = self.source[start..self.at].to_string();
self.push(Kind::Num, text);
Some(())
}
fn digits(&mut self) -> usize {
let start = self.at;
while matches!(self.peek(), Some(b'0'..=b'9')) {
self.at += 1;
}
self.at - start
}
}
#[cfg(test)]
mod tests {
use super::*;
fn shown(value: &str) -> Option<Vec<String>> {
Some(
reindent(value)?
.iter()
.map(|line| line.iter().map(|piece| piece.text.as_str()).collect())
.collect(),
)
}
#[test]
fn an_object_is_broken_out_one_key_to_a_line() {
assert_eq!(
shown(r#"{"id":1,"name":"ada"}"#).unwrap(),
["{", " \"id\": 1,", " \"name\": \"ada\"", "}"]
);
}
#[test]
fn nesting_indents() {
assert_eq!(
shown(r#"{"a":{"b":[1,2]}}"#).unwrap(),
[
"{",
" \"a\": {",
" \"b\": [",
" 1,",
" 2",
" ]",
" }",
"}"
]
);
}
#[test]
fn an_empty_container_stays_on_its_line() {
assert_eq!(
shown(r#"{"a":{},"b":[]}"#).unwrap(),
["{", " \"a\": {},", " \"b\": []", "}"]
);
}
#[test]
fn the_documents_own_bytes_come_back_unchanged() {
let lines = shown(r#"{"b":1.0,"a":2,"b":3,"n":1e3,"z":-0}"#).unwrap();
assert_eq!(
lines,
[
"{",
" \"b\": 1.0,",
" \"a\": 2,",
" \"b\": 3,",
" \"n\": 1e3,",
" \"z\": -0",
"}"
],
"key order kept, duplicates kept, numbers not reformatted"
);
}
#[test]
fn escapes_and_unicode_survive_as_written() {
let lines = shown(r#"{"k":"a\"b\\ é é\nx"}"#).unwrap();
assert_eq!(lines[1], r#" "k": "a\"b\\ é é\nx""#);
}
#[test]
fn the_pieces_say_what_each_run_is() {
let doc = reindent(r#"{"k":"v","n":1,"t":true}"#).unwrap();
let kinds: Vec<Kind> = doc[1].iter().map(|piece| piece.kind).collect();
assert_eq!(
kinds,
[Kind::Punct, Kind::Name, Kind::Punct, Kind::Str, Kind::Punct],
"indent, key, colon, value, comma"
);
assert_eq!(doc[2][3].kind, Kind::Num);
assert_eq!(doc[3][3].kind, Kind::Lit);
}
#[test]
fn a_value_that_is_not_json_is_not_claimed() {
for value in [
"hello",
"42",
"\"a string\"",
"{",
"{}}",
"{\"a\":1,}",
"{\"a\" 1}",
"{a:1}",
"{\"a\":01}",
"{\"a\":1} trailing",
"{\"a\":tru}",
"{\"a\":1.}",
"{\"a\":\"unterminated}",
"not json {\"a\":1}",
] {
assert!(reindent(value).is_none(), "claimed {value:?}");
}
}
#[test]
fn a_newline_inside_a_string_is_not_a_document() {
assert!(reindent("{\"a\": \"one\ntwo\"}").is_none());
}
#[test]
fn whitespace_around_the_document_is_allowed_and_replaced() {
assert_eq!(
shown(" {\n\t\"a\" : 1 } ").unwrap(),
["{", " \"a\": 1", "}"]
);
}
#[test]
fn nesting_past_the_cap_is_refused_rather_than_recursed_into() {
let deep = "[".repeat(MAX_DEPTH + 2) + &"]".repeat(MAX_DEPTH + 2);
assert!(reindent(&deep).is_none());
let shallow = "[".repeat(8) + "1" + &"]".repeat(8);
assert!(reindent(&shallow).is_some());
}
}