1pub(crate) fn strip_jsonc(source: &str) -> String {
8 strip_trailing_commas(&strip_jsonc_comments(source))
9}
10
11pub(crate) fn strip_jsonc_comments(source: &str) -> String {
12 let mut output = String::with_capacity(source.len());
13 let mut chars = source.chars().peekable();
14 let mut in_string = false;
15 let mut escaped = false;
16
17 while let Some(ch) = chars.next() {
18 if in_string {
19 output.push(ch);
20 if escaped {
21 escaped = false;
22 } else if ch == '\\' {
23 escaped = true;
24 } else if ch == '"' {
25 in_string = false;
26 }
27 continue;
28 }
29
30 if ch == '"' {
31 in_string = true;
32 output.push(ch);
33 continue;
34 }
35
36 if ch == '/' {
37 match chars.peek().copied() {
38 Some('/') => {
39 chars.next();
40 for next in chars.by_ref() {
41 if next == '\n' {
42 output.push('\n');
43 break;
44 }
45 }
46 }
47 Some('*') => {
48 chars.next();
49 let mut previous = '\0';
50 for next in chars.by_ref() {
51 if next == '\n' {
52 output.push('\n');
53 }
54 if previous == '*' && next == '/' {
55 break;
56 }
57 previous = next;
58 }
59 }
60 _ => output.push(ch),
61 }
62 continue;
63 }
64
65 output.push(ch);
66 }
67
68 output
69}
70
71pub(crate) fn strip_trailing_commas(source: &str) -> String {
72 let chars = source.chars().collect::<Vec<_>>();
73 let mut output = String::with_capacity(source.len());
74 let mut index = 0usize;
75 let mut in_string = false;
76 let mut escaped = false;
77
78 while index < chars.len() {
79 let ch = chars[index];
80 if in_string {
81 output.push(ch);
82 if escaped {
83 escaped = false;
84 } else if ch == '\\' {
85 escaped = true;
86 } else if ch == '"' {
87 in_string = false;
88 }
89 index += 1;
90 continue;
91 }
92
93 if ch == '"' {
94 in_string = true;
95 output.push(ch);
96 index += 1;
97 continue;
98 }
99
100 if ch == ',' {
101 let mut next = index + 1;
102 while next < chars.len() && chars[next].is_whitespace() {
103 next += 1;
104 }
105 if next < chars.len() && matches!(chars[next], '}' | ']') {
106 index += 1;
107 continue;
108 }
109 }
110
111 output.push(ch);
112 index += 1;
113 }
114
115 output
116}
117
118#[cfg(test)]
119mod tests {
120 use serde_json::Value;
121
122 use super::{strip_jsonc, strip_jsonc_comments, strip_trailing_commas};
123
124 #[test]
125 fn strip_jsonc_comments_preserves_comment_like_text_inside_strings() {
126 let source = r#"{
127 "url": "https://example.com//path",
128 "escaped": "\"// not a comment\"",
129 "block": "/* not a comment */"
130}
131// real comment
132"#;
133
134 assert_eq!(
135 strip_jsonc_comments(source),
136 r#"{
137 "url": "https://example.com//path",
138 "escaped": "\"// not a comment\"",
139 "block": "/* not a comment */"
140}
141
142"#
143 );
144 }
145
146 #[test]
147 fn strip_jsonc_comments_preserves_newlines_from_block_comments() {
148 let source = "{\n/* first\nsecond */\n\"enabled\": true\n}\n";
149
150 assert_eq!(
151 strip_jsonc_comments(source),
152 "{\n\n\n\"enabled\": true\n}\n"
153 );
154 }
155
156 #[test]
157 fn strip_trailing_commas_removes_only_commas_before_closing_brackets() {
158 let source = "{\n \"items\": [1, 2,],\n \"nested\": {\n \"ok\": true,\n },\n}";
159
160 assert_eq!(
161 strip_trailing_commas(source),
162 "{\n \"items\": [1, 2],\n \"nested\": {\n \"ok\": true\n }\n}"
163 );
164 }
165
166 #[test]
167 fn strip_trailing_commas_preserves_commas_inside_strings() {
168 let source = r#"{"message": "comma, // and /* stay */",}"#;
169
170 assert_eq!(
171 strip_trailing_commas(source),
172 r#"{"message": "comma, // and /* stay */"}"#
173 );
174 }
175
176 #[test]
177 fn strip_jsonc_removes_comments_and_trailing_commas() {
178 let source = r#"{
179 // line comment
180 "search_index": true,
181 "formatter": {
182 "rust": "rustfmt", /* block comment */
183 },
184}"#;
185
186 let value = serde_json::from_str::<Value>(&strip_jsonc(source)).unwrap();
187 assert_eq!(value["search_index"], Value::Bool(true));
188 assert_eq!(value["formatter"]["rust"], Value::String("rustfmt".into()));
189 }
190}