Skip to content

Instantly share code, notes, and snippets.

@Tamschi
Last active March 17, 2022 19:38
Show Gist options
  • Save Tamschi/f4e23db5142e19768a96cfd7020dffb7 to your computer and use it in GitHub Desktop.
Save Tamschi/f4e23db5142e19768a96cfd7020dffb7 to your computer and use it in GitHub Desktop.
Compact JSON Prettifier (MIT-licensed)
//! 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