Skip to main content

jsonette/
formatter.rs

1/*
2 * Copyright (c) 2026 DevEtte.
3 *
4 * This project is dual-licensed under both the MIT License and the
5 * Apache License, Version 2.0 (the "License"). You may not use this
6 * file except in compliance with one of these licenses.
7 *
8 * You may obtain a copy of the Licenses at:
9 * - MIT: https://opensource.org
10 * - Apache 2.0: http://apache.org
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19//! Pretty printing and minification for the JSON AST.
20
21use crate::json_node::JsonNode;
22use crate::types::LineEnding;
23
24/// Formats the JSON node.
25/// Uses lossless number representations to preserve precision.
26/// Retrieves configuration settings directly from the global singleton.
27///
28/// # Arguments
29///
30/// * `node` - A reference to the root `JsonNode` to format.
31///
32/// # Returns
33///
34/// The pretty-printed JSON string representation of the node.
35pub fn format(node: &JsonNode) -> String {
36    let opts = crate::settings::get_settings().format;
37    format_impl(node, 0, &opts)
38}
39
40/// Minifies the JSON node, stripping all whitespace and formatting.
41///
42/// # Arguments
43///
44/// * `node` - A reference to the root `JsonNode` to minify.
45///
46/// # Returns
47///
48/// A single-line JSON string representation of the node without any unnecessary whitespace.
49pub fn minify(node: &JsonNode) -> String {
50    minify_impl(node)
51}
52
53/// Computes the indentation prefix string for a given level of nesting.
54/// Supports both tab-based and space-based indentation.
55/// Retrieves active preferences from the global settings manager.
56///
57/// # Arguments
58///
59/// * `level` - The current depth level of nesting.
60///
61/// # Returns
62///
63/// A `String` consisting of space or tab characters.
64fn get_indent(level: usize, opts: &crate::types::FormatOptions) -> String {
65    if level == 0 {
66        return String::new();
67    }
68    if opts.use_tabs {
69        "\t".repeat(level)
70    } else {
71        " ".repeat(level * opts.indent as usize)
72    }
73}
74
75/// Returns the line ending character sequence corresponding to the configuration.
76/// Retrieves active preferences from the global settings manager.
77///
78/// # Returns
79///
80/// A static string slice (`"\n"` or `"\r\n"`).
81fn get_line_ending(opts: &crate::types::FormatOptions) -> &'static str {
82    match opts.line_ending {
83        LineEnding::LF => "\n",
84        LineEnding::CRLF => "\r\n",
85    }
86}
87
88/// Attempts to format a JSON node into a single-line string representation.
89/// Used recursively to inline compact objects and arrays if they are empty or small.
90/// Retrieves active preferences from the global settings manager.
91///
92/// # Arguments
93///
94/// * `node` - A reference to the `JsonNode` to inline-format.
95///
96/// # Returns
97///
98/// `Some(String)` if the node could be formatted inline within 80 characters, or `None` otherwise.
99fn format_inline(node: &JsonNode, opts: &crate::types::FormatOptions) -> Option<String> {
100    match node {
101        JsonNode::Null(_) => Some("null".to_string()),
102        JsonNode::Bool(b, _) => Some(if *b { "true" } else { "false" }.to_string()),
103        JsonNode::Number(_, raw, _) => Some(raw.clone()),
104        JsonNode::String(s, _) => Some(serde_json::to_string(s).unwrap()),
105        JsonNode::Array(elements, _) => {
106            if elements.is_empty() {
107                return Some("[]".to_string());
108            }
109            let mut parts = Vec::with_capacity(elements.len());
110            for elem in elements {
111                let part = format_inline(elem, opts)?;
112                parts.push(part);
113            }
114            let comma_space = if opts.space_after_comma { ", " } else { "," };
115            let joined = parts.join(comma_space);
116            let result = format!("[{}]", joined);
117            // Limit to 80 characters for inline format
118            if result.len() <= 80 {
119                Some(result)
120            } else {
121                None
122            }
123        }
124        JsonNode::Object(pairs, _) => {
125            if pairs.is_empty() {
126                return Some("{}".to_string());
127            }
128            let mut pairs = pairs.clone();
129            if opts.sort_keys {
130                pairs.sort_by(|a, b| a.key.cmp(&b.key));
131            }
132            let mut parts = Vec::with_capacity(pairs.len());
133            for pair in &pairs {
134                let val_str = format_inline(&pair.value, opts)?;
135                let key_str = serde_json::to_string(&pair.key).unwrap();
136                let colon_space = if opts.space_after_colon { ": " } else { ":" };
137                parts.push(format!("{}{}{}", key_str, colon_space, val_str));
138            }
139            let comma_space = if opts.space_after_comma { ", " } else { "," };
140            let joined = parts.join(comma_space);
141            let result = format!("{{{}}}", joined);
142            // Limit to 80 characters for inline format
143            if result.len() <= 80 {
144                Some(result)
145            } else {
146                None
147            }
148        }
149    }
150}
151
152/// Recursively formats a JSON node into a pretty-printed string at a specific indent level.
153/// Retrieves active preferences from the global settings manager.
154///
155/// # Arguments
156///
157/// * `node` - A reference to the `JsonNode` to format.
158/// * `level` - The current indentation depth level.
159///
160/// # Returns
161///
162/// The pretty-printed string representation of the sub-tree.
163fn format_impl(node: &JsonNode, level: usize, opts: &crate::types::FormatOptions) -> String {
164    match node {
165        JsonNode::Null(_) => "null".to_string(),
166        JsonNode::Bool(b, _) => if *b { "true" } else { "false" }.to_string(),
167        JsonNode::Number(_, raw, _) => raw.clone(),
168        JsonNode::String(s, _) => serde_json::to_string(s).unwrap(),
169        JsonNode::Array(elements, _) => {
170            if elements.is_empty() {
171                return "[]".to_string();
172            }
173            if let (crate::types::FoldingStyle::Compact, Some(inline)) =
174                (&opts.folding_style, format_inline(node, opts))
175            {
176                return inline;
177            }
178
179            let line_ending = get_line_ending(opts);
180            let next_indent = get_indent(level + 1, opts);
181            let current_indent = get_indent(level, opts);
182
183            let mut result = String::new();
184            result.push('[');
185            result.push_str(line_ending);
186
187            for (i, elem) in elements.iter().enumerate() {
188                result.push_str(&next_indent);
189                result.push_str(&format_impl(elem, level + 1, opts));
190                if i + 1 < elements.len() {
191                    result.push(',');
192                }
193                result.push_str(line_ending);
194            }
195
196            result.push_str(&current_indent);
197            result.push(']');
198            result
199        }
200        JsonNode::Object(pairs, _) => {
201            if pairs.is_empty() {
202                return "{}".to_string();
203            }
204            if let (crate::types::FoldingStyle::Compact, Some(inline)) =
205                (&opts.folding_style, format_inline(node, opts))
206            {
207                return inline;
208            }
209
210            let mut pairs = pairs.clone();
211            if opts.sort_keys {
212                pairs.sort_by(|a, b| a.key.cmp(&b.key));
213            }
214
215            let line_ending = get_line_ending(opts);
216            let next_indent = get_indent(level + 1, opts);
217            let current_indent = get_indent(level, opts);
218            let colon_str = if opts.space_after_colon { ": " } else { ":" };
219
220            let mut result = String::new();
221            result.push('{');
222            result.push_str(line_ending);
223
224            for (i, pair) in pairs.iter().enumerate() {
225                result.push_str(&next_indent);
226                result.push_str(&serde_json::to_string(&pair.key).unwrap());
227                result.push_str(colon_str);
228                result.push_str(&format_impl(&pair.value, level + 1, opts));
229                if i + 1 < pairs.len() {
230                    result.push(',');
231                }
232                result.push_str(line_ending);
233            }
234
235            result.push_str(&current_indent);
236            result.push('}');
237            result
238        }
239    }
240}
241
242/// Recursively minifies a JSON node, stripping all whitespace and formatting.
243///
244/// # Arguments
245///
246/// * `node` - A reference to the `JsonNode` to minify.
247///
248/// # Returns
249///
250/// The minified string representation of the sub-tree.
251fn minify_impl(node: &JsonNode) -> String {
252    match node {
253        JsonNode::Null(_) => "null".to_string(),
254        JsonNode::Bool(b, _) => if *b { "true" } else { "false" }.to_string(),
255        JsonNode::Number(_, raw, _) => raw.clone(),
256        JsonNode::String(s, _) => serde_json::to_string(s).unwrap(),
257        JsonNode::Array(elements, _) => {
258            let mut parts = Vec::with_capacity(elements.len());
259            for elem in elements {
260                parts.push(minify_impl(elem));
261            }
262            format!("[{}]", parts.join(","))
263        }
264        JsonNode::Object(pairs, _) => {
265            let mut parts = Vec::with_capacity(pairs.len());
266            for pair in pairs {
267                let val_str = minify_impl(&pair.value);
268                let key_str = serde_json::to_string(&pair.key).unwrap();
269                parts.push(format!("{}:{}", key_str, val_str));
270            }
271            format!("{{{}}}", parts.join(","))
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::parser::parse;
280    use crate::types::{FoldingStyle, FormatOptions};
281    use std::sync::Mutex;
282
283    static TEST_LOCK: Mutex<()> = Mutex::new(());
284
285    fn with_temporary_settings<F, R>(opts: FormatOptions, f: F) -> R
286    where
287        F: FnOnce() -> R,
288    {
289        let mut app_settings = crate::settings::get_settings();
290        let original_format = app_settings.format;
291        app_settings.format = opts;
292        crate::settings::update_settings(app_settings).unwrap();
293        let result = f();
294        app_settings.format = original_format;
295        crate::settings::update_settings(app_settings).unwrap();
296        result
297    }
298
299    #[test]
300    fn test_format_primitives() {
301        let _guard = TEST_LOCK.lock().unwrap();
302        let null_node = parse("null").unwrap();
303        assert_eq!(format(&null_node), "null");
304        assert_eq!(minify(&null_node), "null");
305
306        let true_node = parse("true").unwrap();
307        assert_eq!(format(&true_node), "true");
308        assert_eq!(minify(&true_node), "true");
309
310        let false_node = parse("false").unwrap();
311        assert_eq!(format(&false_node), "false");
312        assert_eq!(minify(&false_node), "false");
313
314        let num_node = parse("123.45e-2").unwrap();
315        assert_eq!(format(&num_node), "123.45e-2");
316        assert_eq!(minify(&num_node), "123.45e-2");
317
318        let str_node = parse("\"hello \\u263a world\"").unwrap();
319        assert_eq!(format(&str_node), "\"hello ☺ world\"");
320        assert_eq!(minify(&str_node), "\"hello ☺ world\"");
321    }
322
323    #[test]
324    fn test_format_empty() {
325        let _guard = TEST_LOCK.lock().unwrap();
326        let empty_arr = parse("[]").unwrap();
327        assert_eq!(format(&empty_arr), "[]");
328        assert_eq!(minify(&empty_arr), "[]");
329
330        let empty_obj = parse("{}").unwrap();
331        assert_eq!(format(&empty_obj), "{}");
332        assert_eq!(minify(&empty_obj), "{}");
333    }
334
335    #[test]
336    fn test_format_expanded_array() {
337        let _guard = TEST_LOCK.lock().unwrap();
338        let input = "[1,true,null]";
339        let node = parse(input).unwrap();
340        let expected = "[\n  1,\n  true,\n  null\n]";
341        assert_eq!(format(&node), expected);
342        assert_eq!(minify(&node), "[1,true,null]");
343    }
344
345    #[test]
346    fn test_format_expanded_object() {
347        let _guard = TEST_LOCK.lock().unwrap();
348        let input = "{\"a\":1,\"b\":true}";
349        let node = parse(input).unwrap();
350        let expected = "{\n  \"a\": 1,\n  \"b\": true\n}";
351        assert_eq!(format(&node), expected);
352        assert_eq!(minify(&node), "{\"a\":1,\"b\":true}");
353    }
354
355    #[test]
356    fn test_format_compact_folding() {
357        let _guard = TEST_LOCK.lock().unwrap();
358        let opts = FormatOptions {
359            folding_style: FoldingStyle::Compact,
360            ..Default::default()
361        };
362        let input = "[1,true,null]";
363        let node = parse(input).unwrap();
364        let res = with_temporary_settings(opts, || format(&node));
365        assert_eq!(res, "[1, true, null]");
366
367        let obj_input = "{\"a\":1,\"b\":true}";
368        let obj_node = parse(obj_input).unwrap();
369        let res_obj = with_temporary_settings(opts, || format(&obj_node));
370        assert_eq!(res_obj, "{\"a\": 1, \"b\": true}");
371    }
372
373    #[test]
374    fn test_format_sort_keys() {
375        let _guard = TEST_LOCK.lock().unwrap();
376        let opts = FormatOptions {
377            sort_keys: true,
378            ..Default::default()
379        };
380        let input = "{\"z\":1,\"a\":true}";
381        let node = parse(input).unwrap();
382        let expected = "{\n  \"a\": true,\n  \"z\": 1\n}";
383        let res = with_temporary_settings(opts, || format(&node));
384        assert_eq!(res, expected);
385    }
386}