1use once_cell::sync::Lazy;
2use regex::Regex;
3
4#[derive(Clone, Debug)]
7pub struct FormatInfo {
8 pub sample: Option<String>,
9 pub whitespace_start: String,
10 pub whitespace_end: String,
11}
12
13#[derive(Clone, Debug)]
15pub struct FormatOptions {
16 pub indent: Option<usize>,
19
20 pub preserve_indentation: bool,
23
24 pub preserve_whitespace: bool,
27
28 pub sample_size: usize,
31}
32
33impl Default for FormatOptions {
34 fn default() -> Self {
35 Self {
36 indent: None,
37 preserve_indentation: true,
38 preserve_whitespace: true,
39 sample_size: 1024,
40 }
41 }
42}
43
44pub(crate) fn detect_format(text: &str, opts: &FormatOptions) -> FormatInfo {
45 let sample = if opts.indent.is_none() && opts.preserve_indentation {
46 Some(text.chars().take(opts.sample_size).collect::<String>())
47 } else {
48 None
49 };
50
51 static START_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s+)").unwrap());
52 static END_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\s+)$").unwrap());
53
54 let (whitespace_start, whitespace_end) = if opts.preserve_whitespace {
55 let ws_start = START_RE
56 .captures(text)
57 .and_then(|c| c.get(0))
58 .map(|m| m.as_str().to_string())
59 .unwrap_or_default();
60 let ws_end = END_RE
61 .captures(text)
62 .and_then(|c| c.get(0))
63 .map(|m| m.as_str().to_string())
64 .unwrap_or_default();
65
66 (ws_start, ws_end)
67 } else {
68 (String::new(), String::new())
69 };
70
71 FormatInfo {
72 sample,
73 whitespace_start,
74 whitespace_end,
75 }
76}
77
78pub(crate) fn compute_indent(info: &FormatInfo, opts: &FormatOptions) -> usize {
79 if let Some(explicit) = opts.indent {
80 return explicit;
81 }
82
83 if let Some(sample) = &info.sample {
84 for line in sample.lines() {
87 let trimmed = line.trim_start();
88 if trimmed.is_empty() {
89 continue;
90 }
91 let indent_len = line.len() - trimmed.len();
92 if indent_len > 0 {
93 return indent_len;
94 }
95 }
96 }
97
98 2
100}
101
102#[allow(dead_code)]
105pub(crate) fn strip_line_comments(s: &str, prefix: &str) -> String {
106 let mut out = String::with_capacity(s.len());
107 for (i, line) in s.lines().enumerate() {
108 if i > 0 {
109 out.push('\n');
110 }
111 if let Some(pos) = line.find(prefix) {
112 out.push_str(&line[..pos]);
113 } else {
114 out.push_str(line);
115 }
116 }
117 out
118}
119
120#[derive(Clone, Debug)]
122pub struct Formatted<T> {
123 pub value: T,
124 pub format: FormatInfo,
125}
126
127impl<T> Formatted<T> {
128 pub fn new(text: &str, value: T, opts: &FormatOptions) -> Self {
129 let format = detect_format(text, opts);
130 Self { value, format }
131 }
132}
133
134pub(crate) fn wrap_whitespace(content: &str, info: &FormatInfo) -> String {
136 let cap = info.whitespace_start.len() + content.len() + info.whitespace_end.len();
137 let mut s = String::with_capacity(cap);
138 s.push_str(&info.whitespace_start);
139 s.push_str(content);
140 s.push_str(&info.whitespace_end);
141 s
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn detect_captures_outer_whitespace_and_sample() {
150 let text = "\n {\"a\": 1}\n\n";
151 let opts = FormatOptions::default();
152 let info = detect_format(text, &opts);
153
154 assert_eq!(info.whitespace_start, "\n ");
157 assert_eq!(info.whitespace_end, "\n\n");
158 assert!(info.sample.is_some());
159 assert!(info.sample.as_ref().unwrap().contains("{\"a\": 1}"));
160 }
161
162 #[test]
163 fn detect_respects_preserve_flags() {
164 let text = " {\"a\": 1} ";
165 let mut opts = FormatOptions::default();
166 opts.preserve_whitespace = false;
167 opts.preserve_indentation = false;
168
169 let info = detect_format(text, &opts);
170 assert!(info.sample.is_none());
171 assert!(info.whitespace_start.is_empty());
172 assert!(info.whitespace_end.is_empty());
173 }
174
175 #[test]
176 fn compute_indent_prefers_explicit() {
177 let info = FormatInfo {
178 sample: Some(" key: 1".into()),
179 whitespace_start: String::new(),
180 whitespace_end: String::new(),
181 };
182 let mut opts = FormatOptions::default();
183 opts.indent = Some(4);
184
185 assert_eq!(compute_indent(&info, &opts), 4);
186 }
187
188 #[test]
189 fn compute_indent_detects_from_sample() {
190 let info = FormatInfo {
191 sample: Some(" key: 1\n child: 2".into()),
192 whitespace_start: String::new(),
193 whitespace_end: String::new(),
194 };
195 let opts = FormatOptions::default();
196
197 assert_eq!(compute_indent(&info, &opts), 2);
198 }
199
200 #[test]
201 fn compute_indent_falls_back_to_default() {
202 let info = FormatInfo {
203 sample: Some("\n\n".into()),
204 whitespace_start: String::new(),
205 whitespace_end: String::new(),
206 };
207 let opts = FormatOptions::default();
208
209 assert_eq!(compute_indent(&info, &opts), 2);
210 }
211}