pub mod args;
pub mod conversion;
pub mod json_stream;
pub mod json_stringify;
use crate::error::{Result, ToonError};
use crate::options::{DecodeOptions, EncodeOptions, ExpandPathsMode, KeyFoldingMode};
use args::{Args, ExpandPathsArg, KeyFoldingArg, Mode};
use clap::Parser;
use std::fs::File;
use std::io::{self, BufWriter, Read, Write};
use std::path::Path;
pub fn run() -> Result<()> {
let args = Args::parse();
let mode = args.detect_mode();
match mode {
Mode::Encode => run_encode(&args),
Mode::Decode => run_decode(&args),
}
}
fn run_encode(args: &Args) -> Result<()> {
let input = read_input(args)?;
let options = EncodeOptions {
indent: Some(usize::from(args.indent)),
delimiter: Some(args.delimiter),
key_folding: Some(match args.key_folding {
KeyFoldingArg::Off => KeyFoldingMode::Off,
KeyFoldingArg::Safe => KeyFoldingMode::Safe,
}),
flatten_depth: args.flatten_depth,
replacer: None,
};
let toon_lines = conversion::encode_to_toon_lines(&input, Some(options))?;
if args.stats {
let toon_output = toon_lines.join("\n");
write_output(args, toon_output.as_bytes())?;
let json_tokens = estimate_tokens(&input);
let toon_tokens = estimate_tokens(&toon_output);
let diff = json_tokens.saturating_sub(toon_tokens);
#[allow(clippy::cast_precision_loss)]
let percent = if json_tokens > 0 {
(diff as f64 / json_tokens as f64) * 100.0
} else {
0.0
};
eprintln!();
eprintln!("Token estimates: ~{json_tokens} (JSON) → ~{toon_tokens} (TOON)");
if diff > 0 {
eprintln!("Saved ~{diff} tokens (-{percent:.1}%)");
}
} else {
write_lines(args, &toon_lines)?;
}
if let Some(ref output_path) = args.output {
let input_label = format_input_label(args);
let output_label = output_path.display();
eprintln!("Encoded `{input_label}` → `{output_label}`");
}
Ok(())
}
fn run_decode(args: &Args) -> Result<()> {
let input = read_input(args)?;
let options = DecodeOptions {
indent: Some(usize::from(args.indent)),
strict: Some(!args.no_strict),
expand_paths: Some(match args.expand_paths {
ExpandPathsArg::Off => ExpandPathsMode::Off,
ExpandPathsArg::Safe => ExpandPathsMode::Safe,
}),
};
let json_chunks = conversion::decode_to_json_chunks(&input, Some(options))?;
write_chunks(args, &json_chunks)?;
if let Some(ref output_path) = args.output {
let input_label = format_input_label(args);
let output_label = output_path.display();
eprintln!("Decoded `{input_label}` → `{output_label}`");
}
Ok(())
}
fn read_input(args: &Args) -> Result<String> {
if args.is_stdin() {
read_stdin()
} else {
let path = args
.input
.as_ref()
.ok_or_else(|| ToonError::message("No input file specified"))?;
read_file(path)
}
}
fn read_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.map_err(ToonError::stdin_read)?;
Ok(buffer)
}
fn read_file(path: &Path) -> Result<String> {
std::fs::read_to_string(path).map_err(|e| ToonError::file_read(path.to_path_buf(), e))
}
fn write_output(args: &Args, data: &[u8]) -> Result<()> {
if let Some(ref path) = args.output {
let mut file = File::create(path).map_err(|e| ToonError::file_create(path.clone(), e))?;
file.write_all(data)
.map_err(|e| ToonError::file_write(path.clone(), e))?;
file.write_all(b"\n")
.map_err(|e| ToonError::file_write(path.clone(), e))?;
} else {
let stdout = io::stdout();
let mut handle = stdout.lock();
handle.write_all(data).map_err(ToonError::stdout_write)?;
handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
}
Ok(())
}
fn write_lines(args: &Args, lines: &[String]) -> Result<()> {
if let Some(ref path) = args.output {
let file = File::create(path).map_err(|e| ToonError::file_create(path.clone(), e))?;
let mut writer = BufWriter::new(file);
for (i, line) in lines.iter().enumerate() {
if i > 0 {
writer
.write_all(b"\n")
.map_err(|e| ToonError::file_write(path.clone(), e))?;
}
writer
.write_all(line.as_bytes())
.map_err(|e| ToonError::file_write(path.clone(), e))?;
}
writer
.write_all(b"\n")
.map_err(|e| ToonError::file_write(path.clone(), e))?;
} else {
let stdout = io::stdout();
let mut handle = stdout.lock();
for (i, line) in lines.iter().enumerate() {
if i > 0 {
handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
}
handle
.write_all(line.as_bytes())
.map_err(ToonError::stdout_write)?;
}
handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
}
Ok(())
}
fn write_chunks(args: &Args, chunks: &[String]) -> Result<()> {
if let Some(ref path) = args.output {
let file = File::create(path).map_err(|e| ToonError::file_create(path.clone(), e))?;
let mut writer = BufWriter::new(file);
for chunk in chunks {
writer
.write_all(chunk.as_bytes())
.map_err(|e| ToonError::file_write(path.clone(), e))?;
}
writer
.write_all(b"\n")
.map_err(|e| ToonError::file_write(path.clone(), e))?;
} else {
let stdout = io::stdout();
let mut handle = stdout.lock();
for chunk in chunks {
handle
.write_all(chunk.as_bytes())
.map_err(ToonError::stdout_write)?;
}
handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
}
Ok(())
}
fn format_input_label(args: &Args) -> String {
if args.is_stdin() {
"stdin".to_string()
} else if let Some(ref path) = args.input {
path.display().to_string()
} else {
"stdin".to_string()
}
}
fn estimate_tokens(text: &str) -> usize {
let char_estimate = text.chars().filter(|c| !c.is_whitespace()).count() / 4;
let word_estimate = text.split_whitespace().count();
char_estimate.max(word_estimate).max(1)
}