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