Last active
March 17, 2022 19:38
-
-
Save Tamschi/f4e23db5142e19768a96cfd7020dffb7 to your computer and use it in GitHub Desktop.
Compact JSON Prettifier (MIT-licensed)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! Formats JSON in a slightly more compact way, by not introducing line breaks between array elements. | |
//! | |
//! Any non-coding ASCII-whitespace is removed or replaced with `b' '` (simple space) and/or `b'\n'`. | |
/** | |
* MIT License | |
* | |
* Copyright (c) 2022 Tamme Schichler <tamme@schichler.dev> | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
use core::slice; | |
use std::io::{self, Read, Write}; | |
use vec1::vec1; | |
/// Formats JSON in a slightly more compact way, by not introducing line breaks between array elements. | |
/// DOES NOT VALIDATE and MAY TURN INVALID JSON VALID. | |
/// | |
/// An output buffer of the length of the input buffer is preallocated. | |
pub fn prettify_string_insecure(input: &str) -> io::Result<String> { | |
let mut output = Vec::with_capacity(input.len()); | |
prettify_utf8_stream_insecure(input.as_bytes(), &mut output).map(|()| unsafe { | |
//SAFETY: The formatter here hasn't created invalid UTF-8 following the garbage in ⇔ garbage out principle | |
// (and also because it processes to error or end no matter what). | |
String::from_utf8_unchecked(output) | |
}) | |
} | |
/// Formats JSON in a slightly more compact way, by not introducing line breaks between array elements. | |
/// DOES NOT VALIDATE and MAY TURN INVALID JSON VALID. | |
/// | |
/// # Panics | |
/// | |
/// Iff `input` misbehaves by reporting a multi-byte read into a one byte buffer. | |
pub fn prettify_utf8_stream_insecure<R: Read, W: Write>(ref mut input: R, output: &mut W) -> io::Result<()> { | |
// It's more convenient this way. | |
#![allow(clippy::toplevel_ref_arg)] | |
assert!('\r'.is_ascii_whitespace()); | |
let mut objectivity = vec1![false]; | |
let mut indent = 0_usize; | |
while let Some(b) = read_one(input)? { | |
match b { | |
_ if char::is_ascii_whitespace(&(b as char)) => (), | |
b'{' => { | |
write_one(output, b)?; | |
objectivity.push(true); | |
indent = indent.saturating_add(1); | |
break_line(output, indent)?; | |
} | |
b'}' => { | |
objectivity.pop().ok(); | |
indent = indent.saturating_sub(1); | |
break_line(output, indent)?; | |
write_one(output, b)?; | |
} | |
b'[' => { | |
write_one(output, b)?; | |
objectivity.push(false); | |
indent = indent.saturating_add(1); | |
} | |
b']' => { | |
objectivity.pop().ok(); | |
indent = indent.saturating_sub(1); | |
write_one(output, b)?; | |
} | |
b':' => { | |
write!(output, ": ")?; | |
} | |
b',' => { | |
if *objectivity.last() { | |
write_one(output, b)?; | |
break_line(output, indent)?; | |
} else { | |
// "Fortunately" JSON doesn't support trailing commas, | |
// so this doesn't cause unusual extra spaces ✨ | |
write!(output, ", ")?; | |
} | |
} | |
b'"' => { | |
write_one(output, b)?; | |
'string: while let Some(b) = read_one(input)? { | |
match b { | |
b'\\' => { | |
write_one(output, b)?; | |
// Ensure any following `b'\\'` or `b'"'` is also skipped: | |
// This works because multibyte char bytes can't be in the single byte char value range, | |
// and because no remaining escape sequence rests can contain `b'\\'`: | |
write_one( | |
output, | |
read_one(input)?.ok_or(io::ErrorKind::UnexpectedEof)?, | |
)?; | |
// Now if a char was hacked apart, we can just skip over it in the following iterations. | |
} | |
b'"' => { | |
write_one(output, b)?; | |
break 'string; | |
} | |
b => write_one(output, b)?, | |
} | |
} | |
} | |
b => write_one(output, b)?, | |
} | |
} | |
Ok(()) | |
} | |
fn read_one(input: &mut impl Read) -> io::Result<Option<u8>> { | |
let mut slot = u8::default(); | |
match input.read(slice::from_mut(&mut slot)) { | |
Ok(1) => Ok(Some(slot)), | |
Ok(0) => Ok(None), | |
Ok(len) => panic!("Read of {} bytes into 1-sized buffer.", len), | |
Err(error) => Err(error), | |
} | |
} | |
fn write_one(output: &mut impl Write, byte: u8) -> io::Result<()> { | |
output.write_all(slice::from_ref(&byte)) | |
} | |
fn break_line(output: &mut impl Write, indent: usize) -> io::Result<()> { | |
writeln!(output)?; | |
for _ in 0..indent { | |
write!(output, " ")?; | |
} | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment