1pub mod args;
2pub mod conversion;
3pub mod json_stream;
4pub mod json_stringify;
5
6use crate::error::{Result, ToonError};
7use crate::options::{DecodeOptions, EncodeOptions, ExpandPathsMode, KeyFoldingMode};
8use args::{Args, ExpandPathsArg, KeyFoldingArg, Mode};
9use clap::Parser;
10use std::fs::File;
11use std::io::{self, BufWriter, Read, Write};
12use std::path::Path;
13
14pub fn run() -> Result<()> {
20 let args = Args::parse();
21 let mode = args.detect_mode();
22
23 match mode {
24 Mode::Encode => run_encode(&args),
25 Mode::Decode => run_decode(&args),
26 }
27}
28
29fn run_encode(args: &Args) -> Result<()> {
30 let input = read_input(args)?;
32
33 let options = EncodeOptions {
35 indent: Some(usize::from(args.indent)),
36 delimiter: Some(args.delimiter),
37 key_folding: Some(match args.key_folding {
38 KeyFoldingArg::Off => KeyFoldingMode::Off,
39 KeyFoldingArg::Safe => KeyFoldingMode::Safe,
40 }),
41 flatten_depth: args.flatten_depth,
42 replacer: None,
43 };
44
45 let toon_lines = conversion::encode_to_toon_lines(&input, Some(options))?;
47
48 if args.stats {
50 let toon_output = toon_lines.join("\n");
51 write_output(args, toon_output.as_bytes())?;
52
53 let json_tokens = estimate_tokens(&input);
55 let toon_tokens = estimate_tokens(&toon_output);
56 let diff = json_tokens.saturating_sub(toon_tokens);
57 #[allow(clippy::cast_precision_loss)]
58 let percent = if json_tokens > 0 {
59 (diff as f64 / json_tokens as f64) * 100.0
60 } else {
61 0.0
62 };
63
64 eprintln!();
66 eprintln!("Token estimates: ~{json_tokens} (JSON) → ~{toon_tokens} (TOON)");
67 if diff > 0 {
68 eprintln!("Saved ~{diff} tokens (-{percent:.1}%)");
69 }
70 } else {
71 write_lines(args, &toon_lines)?;
73 }
74
75 if let Some(ref output_path) = args.output {
77 let input_label = format_input_label(args);
78 let output_label = output_path.display();
79 eprintln!("Encoded `{input_label}` → `{output_label}`");
80 }
81
82 Ok(())
83}
84
85fn run_decode(args: &Args) -> Result<()> {
86 let input = read_input(args)?;
88
89 let options = DecodeOptions {
91 indent: Some(usize::from(args.indent)),
92 strict: Some(!args.no_strict),
93 expand_paths: Some(match args.expand_paths {
94 ExpandPathsArg::Off => ExpandPathsMode::Off,
95 ExpandPathsArg::Safe => ExpandPathsMode::Safe,
96 }),
97 };
98
99 let json_chunks = conversion::decode_to_json_chunks(&input, Some(options))?;
101
102 write_chunks(args, &json_chunks)?;
104
105 if let Some(ref output_path) = args.output {
107 let input_label = format_input_label(args);
108 let output_label = output_path.display();
109 eprintln!("Decoded `{input_label}` → `{output_label}`");
110 }
111
112 Ok(())
113}
114
115fn read_input(args: &Args) -> Result<String> {
116 if args.is_stdin() {
117 read_stdin()
118 } else {
119 let path = args
120 .input
121 .as_ref()
122 .ok_or_else(|| ToonError::message("No input file specified"))?;
123 read_file(path)
124 }
125}
126
127fn read_stdin() -> Result<String> {
128 let mut buffer = String::new();
129 io::stdin()
130 .read_to_string(&mut buffer)
131 .map_err(ToonError::stdin_read)?;
132 Ok(buffer)
133}
134
135fn read_file(path: &Path) -> Result<String> {
136 std::fs::read_to_string(path).map_err(|e| ToonError::file_read(path.to_path_buf(), e))
137}
138
139fn write_output(args: &Args, data: &[u8]) -> Result<()> {
140 if let Some(ref path) = args.output {
141 let mut file = File::create(path).map_err(|e| ToonError::file_create(path.clone(), e))?;
142 file.write_all(data)
143 .map_err(|e| ToonError::file_write(path.clone(), e))?;
144 file.write_all(b"\n")
146 .map_err(|e| ToonError::file_write(path.clone(), e))?;
147 } else {
148 let stdout = io::stdout();
149 let mut handle = stdout.lock();
150 handle.write_all(data).map_err(ToonError::stdout_write)?;
151 handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
152 }
153 Ok(())
154}
155
156fn write_lines(args: &Args, lines: &[String]) -> Result<()> {
157 if let Some(ref path) = args.output {
158 let file = File::create(path).map_err(|e| ToonError::file_create(path.clone(), e))?;
159 let mut writer = BufWriter::new(file);
160
161 for (i, line) in lines.iter().enumerate() {
162 if i > 0 {
163 writer
164 .write_all(b"\n")
165 .map_err(|e| ToonError::file_write(path.clone(), e))?;
166 }
167 writer
168 .write_all(line.as_bytes())
169 .map_err(|e| ToonError::file_write(path.clone(), e))?;
170 }
171 writer
173 .write_all(b"\n")
174 .map_err(|e| ToonError::file_write(path.clone(), e))?;
175 } else {
176 let stdout = io::stdout();
177 let mut handle = stdout.lock();
178
179 for (i, line) in lines.iter().enumerate() {
180 if i > 0 {
181 handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
182 }
183 handle
184 .write_all(line.as_bytes())
185 .map_err(ToonError::stdout_write)?;
186 }
187 handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
189 }
190 Ok(())
191}
192
193fn write_chunks(args: &Args, chunks: &[String]) -> Result<()> {
194 if let Some(ref path) = args.output {
195 let file = File::create(path).map_err(|e| ToonError::file_create(path.clone(), e))?;
196 let mut writer = BufWriter::new(file);
197
198 for chunk in chunks {
199 writer
200 .write_all(chunk.as_bytes())
201 .map_err(|e| ToonError::file_write(path.clone(), e))?;
202 }
203 writer
205 .write_all(b"\n")
206 .map_err(|e| ToonError::file_write(path.clone(), e))?;
207 } else {
208 let stdout = io::stdout();
209 let mut handle = stdout.lock();
210
211 for chunk in chunks {
212 handle
213 .write_all(chunk.as_bytes())
214 .map_err(ToonError::stdout_write)?;
215 }
216 handle.write_all(b"\n").map_err(ToonError::stdout_write)?;
218 }
219 Ok(())
220}
221
222fn format_input_label(args: &Args) -> String {
223 if args.is_stdin() {
224 "stdin".to_string()
225 } else if let Some(ref path) = args.input {
226 path.display().to_string()
227 } else {
228 "stdin".to_string()
229 }
230}
231
232fn estimate_tokens(text: &str) -> usize {
235 let char_estimate = text.chars().filter(|c| !c.is_whitespace()).count() / 4;
237 let word_estimate = text.split_whitespace().count();
238 char_estimate.max(word_estimate).max(1)
239}