Custom JSON formatter that works with template syntax (#128)

This commit is contained in:
Gregory Schier
2024-10-21 22:17:14 +00:00
committed by GitHub
parent aa7f18a16f
commit e216214085
17 changed files with 414 additions and 117 deletions

View File

@@ -0,0 +1,230 @@
enum FormatState {
TemplateTag,
String,
None,
}
/// Formats JSON that might contain template tags (skipped entirely)
pub fn format_json(text: &str, tab: &str) -> String {
let mut chars = text.chars().peekable();
let mut new_json = "".to_string();
let mut depth = 0;
let mut state = FormatState::None;
loop {
let rest_of_chars = chars.clone();
let current_char = match chars.next() {
None => break,
Some(c) => c,
};
// Handle JSON string states
if let FormatState::String = state {
match current_char {
'"' => {
state = FormatState::None;
new_json.push(current_char);
continue;
}
'\\' => {
new_json.push(current_char);
if let Some(c) = chars.next() {
new_json.push(c);
}
continue;
}
_ => {
new_json.push(current_char);
continue;
}
}
}
// Close Template tag states
if let FormatState::TemplateTag = state {
if rest_of_chars.take(2).collect::<String>() == "]}" {
state = FormatState::None;
new_json.push_str("]}");
chars.next(); // Skip the second closing bracket
continue;
} else {
new_json.push(current_char);
continue;
}
}
if rest_of_chars.take(3).collect::<String>() == "${[" {
state = FormatState::TemplateTag;
new_json.push_str("${[");
chars.next(); // Skip {
chars.next(); // Skip [
continue;
}
match current_char {
',' => {
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
'{' => match chars.peek() {
Some('}') => {
new_json.push(current_char);
new_json.push('}');
}
_ => {
depth += 1;
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
},
'[' => match chars.peek() {
Some(']') => {
new_json.push(current_char);
new_json.push(']');
}
_ => {
depth += 1;
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
},
'}' => {
depth -= 1;
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
new_json.push(current_char);
// Pad with space if the next char is not a comma
if let Some(',') = chars.peek() {
new_json.push(' ');
}
}
']' => {
depth -= 1;
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
new_json.push(current_char);
// Pad with space if the next char is not a comma
if let Some(',') = chars.peek() {
new_json.push(' ');
}
}
':' => {
new_json.push(current_char);
new_json.push(' '); // Pad with space
}
'"' => {
state = FormatState::String;
new_json.push(current_char);
}
_ => {
if current_char == ' '
|| current_char == '\n'
|| current_char == '\t'
|| current_char == '\r'
{
// Don't add these
} else {
new_json.push(current_char);
}
}
}
}
// Replace only lines containing whitespace with nothing
new_json
.lines()
.filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines
.collect::<Vec<&str>>() // Collect the non-empty lines into a vector
.join("\n") // Join the lines back into a single string
}
#[cfg(test)]
mod test {
use crate::format::format_json;
#[test]
fn test_simple_object() {
assert_eq!(
format_json(r#"{"foo":"bar","baz":"qux"}"#, " "),
r#"
{
"foo": "bar",
"baz": "qux"
}
"#
.trim()
);
}
#[test]
fn test_simple_array() {
assert_eq!(
format_json(r#"["foo","bar","baz","qux"]"#, " "),
r#"
[
"foo",
"bar",
"baz",
"qux"
]
"#
.trim()
);
}
#[test]
fn test_extra_whitespace() {
assert_eq!(
format_json(
r#"["foo", "bar", "baz","qux"
]"#,
" "
),
r#"
[
"foo",
"bar",
"baz",
"qux"
]
"#
.trim()
);
}
#[test]
fn test_invalid_json() {
assert_eq!(
format_json(r#"["foo", {"bar", }"baz",["qux" ]]"#, " "),
r#"
[
"foo",
{
"bar",
}"baz",
[
"qux"
]
]
"#
.trim()
);
}
#[test]
fn test_skip_template_tags() {
assert_eq!(
format_json(r#"{"foo":${[ fn("hello", "world") ]} }"#, " "),
r#"
{
"foo": ${[ fn("hello", "world") ]}
}
"#
.trim()
);
}
}

View File

@@ -1,5 +1,6 @@
pub mod parser;
pub mod renderer;
pub mod format;
pub use parser::*;
pub use renderer::*;