1use super::c_common::{Token, TokenKind, safe_block_comment};
24use super::{MinifyError, MinifyOptions, MinifyOutput, MinifyWarning};
25
26pub fn minify(
27 source: &str,
28 opts: &MinifyOptions,
29 is_cpp: bool,
30) -> Result<MinifyOutput, MinifyError> {
31 let toks = tokenize(source, is_cpp)?;
32 emit(&toks, opts.keep_comments)
33}
34
35fn tokenize(src: &str, is_cpp: bool) -> Result<Vec<Token<'_>>, MinifyError> {
36 let bytes = src.as_bytes();
37 let mut out: Vec<Token<'_>> = Vec::new();
38 let mut i = 0usize;
39 let mut at_line_start = true;
40 while i < bytes.len() {
41 let c = bytes[i];
42 if matches!(c, b' ' | b'\t' | b'\r') {
43 i += 1;
44 continue;
45 }
46 if c == b'\n' {
47 out.push(Token::new(TokenKind::Newline));
48 i += 1;
49 at_line_start = true;
50 continue;
51 }
52 if at_line_start && c == b'#' {
56 let start = i;
57 let mut j = i;
58 while j < bytes.len() {
59 if bytes[j] == b'\\' && peek(bytes, j + 1) == Some(b'\n') {
60 j += 2;
61 continue;
62 }
63 if bytes[j] == b'\\'
64 && peek(bytes, j + 1) == Some(b'\r')
65 && peek(bytes, j + 2) == Some(b'\n')
66 {
67 j += 3;
68 continue;
69 }
70 if bytes[j] == b'\n' {
71 break;
72 }
73 j += 1;
74 }
75 out.push(Token::new(TokenKind::Preproc(&src[start..j])));
76 i = j;
77 at_line_start = false;
80 continue;
81 }
82 at_line_start = false;
83 if c == b'/' && peek(bytes, i + 1) == Some(b'/') {
84 let start = i + 2;
88 let mut j = start;
89 while j < bytes.len() {
90 if bytes[j] == b'\\' && peek(bytes, j + 1) == Some(b'\n') {
91 j += 2;
92 continue;
93 }
94 if bytes[j] == b'\\'
95 && peek(bytes, j + 1) == Some(b'\r')
96 && peek(bytes, j + 2) == Some(b'\n')
97 {
98 j += 3;
99 continue;
100 }
101 if bytes[j] == b'\n' {
102 break;
103 }
104 j += 1;
105 }
106 out.push(Token::new(TokenKind::LineComment(&src[start..j])));
107 i = j;
108 continue;
109 }
110 if c == b'/' && peek(bytes, i + 1) == Some(b'*') {
111 let body_start = i + 2;
112 let mut j = body_start;
113 let mut found = false;
114 while j + 1 < bytes.len() {
115 if bytes[j] == b'*' && bytes[j + 1] == b'/' {
116 found = true;
117 break;
118 }
119 j += 1;
120 }
121 if !found {
122 return Err(MinifyError::new("unterminated /* */ block comment"));
123 }
124 out.push(Token::new(TokenKind::BlockComment(&src[body_start..j])));
125 i = j + 2;
126 continue;
127 }
128 if let Some(n) = try_scan_string(src, i, is_cpp)? {
132 out.push(Token::new(TokenKind::StrLit(&src[i..i + n])));
133 i += n;
134 continue;
135 }
136 if c == b'\'' {
137 let n = scan_char_literal(src, i)?;
138 out.push(Token::new(TokenKind::StrLit(&src[i..i + n])));
139 i += n;
140 continue;
141 }
142 if is_word_start(src, i) {
143 let n = scan_word(src, i);
144 out.push(Token::new(TokenKind::Word(&src[i..i + n])));
145 i += n;
146 continue;
147 }
148 let n = scan_multi_punct(bytes, i);
149 out.push(Token::new(TokenKind::Punct(&src[i..i + n])));
150 i += n;
151 }
152 Ok(out)
153}
154
155fn emit(tokens: &[Token<'_>], keep_comments: bool) -> Result<MinifyOutput, MinifyError> {
156 let mut out = String::new();
157 let mut warnings: Vec<MinifyWarning> = Vec::new();
158 let mut prev_emit_last: Option<char> = None;
159 let mut last_was_preproc = false;
160 for tok in tokens {
161 match &tok.kind {
162 TokenKind::Newline => {
163 if last_was_preproc && !out.ends_with('\n') {
167 out.push('\n');
168 prev_emit_last = None;
169 last_was_preproc = false;
170 }
171 }
172 TokenKind::LineComment(body) => {
173 if !keep_comments {
174 continue;
175 }
176 let block = safe_block_comment(body);
177 push_with_space(&mut out, &mut prev_emit_last, &block);
178 warnings.push(MinifyWarning::LineCommentConverted);
179 }
180 TokenKind::BlockComment(body) => {
181 if !keep_comments {
182 continue;
183 }
184 let block = safe_block_comment(body);
185 push_with_space(&mut out, &mut prev_emit_last, &block);
186 }
187 TokenKind::Word(s)
188 | TokenKind::Punct(s)
189 | TokenKind::StrLit(s)
190 | TokenKind::Template(s)
191 | TokenKind::Regex(s) => {
192 push_with_space(&mut out, &mut prev_emit_last, s);
193 last_was_preproc = false;
194 }
195 TokenKind::Preproc(s) => {
196 if !out.is_empty() && !out.ends_with('\n') {
197 out.push('\n');
198 }
199 out.push_str(s);
200 prev_emit_last = None;
201 last_was_preproc = true;
202 }
203 }
204 }
205 if last_was_preproc && !out.ends_with('\n') {
206 out.push('\n');
207 }
208 Ok(MinifyOutput {
209 body: out,
210 warnings,
211 })
212}
213
214fn push_with_space(out: &mut String, prev_emit_last: &mut Option<char>, s: &str) {
215 if s.is_empty() {
216 return;
217 }
218 use super::c_common::needs_space;
219 if let Some(prev) = *prev_emit_last {
220 if let Some(next) = s.chars().next() {
221 if needs_space(prev, next) {
222 out.push(' ');
223 }
224 }
225 }
226 out.push_str(s);
227 *prev_emit_last = s.chars().next_back();
228}
229
230fn try_scan_string(src: &str, i: usize, is_cpp: bool) -> Result<Option<usize>, MinifyError> {
231 let bytes = src.as_bytes();
232 let mut p = i;
235 let mut had_prefix = false;
236 if peek(bytes, p) == Some(b'u') && peek(bytes, p + 1) == Some(b'8') {
238 let after = p + 2;
240 if peek(bytes, after) == Some(b'"')
241 || (is_cpp && peek(bytes, after) == Some(b'R') && peek(bytes, after + 1) == Some(b'"'))
242 {
243 p = after;
244 had_prefix = true;
245 }
246 } else if matches!(peek(bytes, p), Some(b'L') | Some(b'u') | Some(b'U')) {
247 let after = p + 1;
248 if peek(bytes, after) == Some(b'"')
249 || (is_cpp && peek(bytes, after) == Some(b'R') && peek(bytes, after + 1) == Some(b'"'))
250 {
251 p = after;
252 had_prefix = true;
253 }
254 }
255 let raw = is_cpp && peek(bytes, p) == Some(b'R') && peek(bytes, p + 1) == Some(b'"');
256 if raw {
257 p += 1; debug_assert_eq!(bytes[p], b'"');
260 let delim_start = p + 1;
261 let mut j = delim_start;
262 while j < bytes.len() && bytes[j] != b'(' {
263 j += 1;
264 }
265 if j >= bytes.len() {
266 return Err(MinifyError::new("malformed raw string"));
267 }
268 let delim = &bytes[delim_start..j];
269 let body_start = j + 1;
270 let mut k = body_start;
272 loop {
273 if k >= bytes.len() {
274 return Err(MinifyError::new("unterminated raw string"));
275 }
276 if bytes[k] == b')' && k + 1 + delim.len() < bytes.len() {
277 if &bytes[k + 1..k + 1 + delim.len()] == delim
278 && bytes.get(k + 1 + delim.len()) == Some(&b'"')
279 {
280 let total = k + 1 + delim.len() + 1 - i;
281 return Ok(Some(total));
282 }
283 }
284 k += 1;
285 }
286 }
287 if peek(bytes, p) == Some(b'"') {
288 let n = scan_dq_string(src, p)?;
289 return Ok(Some(p + n - i));
290 }
291 if had_prefix {
292 return Ok(None);
294 }
295 Ok(None)
296}
297
298fn scan_dq_string(src: &str, i: usize) -> Result<usize, MinifyError> {
299 let bytes = src.as_bytes();
300 debug_assert_eq!(bytes[i], b'"');
301 let mut j = i + 1;
302 while j < bytes.len() {
303 match bytes[j] {
304 b'\\' => j += 2,
305 b'"' => return Ok(j + 1 - i),
306 b'\n' => return Err(MinifyError::new("newline in string literal")),
307 _ => j += 1,
308 }
309 }
310 Err(MinifyError::new("unterminated string literal"))
311}
312
313fn scan_char_literal(src: &str, i: usize) -> Result<usize, MinifyError> {
314 let bytes = src.as_bytes();
315 debug_assert_eq!(bytes[i], b'\'');
316 let mut j = i + 1;
317 while j < bytes.len() {
318 if bytes[j] == b'\\' {
319 j += 2;
320 continue;
321 }
322 if bytes[j] == b'\'' {
323 return Ok(j + 1 - i);
324 }
325 if bytes[j] == b'\n' {
326 return Err(MinifyError::new("newline in char literal"));
327 }
328 j += 1;
329 }
330 Err(MinifyError::new("unterminated char literal"))
331}
332
333fn is_word_start(src: &str, i: usize) -> bool {
334 let c = char_at(src, i);
335 c.is_alphabetic() || c == '_' || c.is_ascii_digit()
336}
337
338fn scan_word(src: &str, i: usize) -> usize {
339 let bytes = src.as_bytes();
340 let mut j = i;
341 while j < bytes.len() {
342 let c = char_at(src, j);
343 if c.is_alphanumeric() || c == '_' {
344 j += c.len_utf8();
345 continue;
346 }
347 if c == '.' {
348 let next = peek(bytes, j + 1);
349 if matches!(next, Some(b'0'..=b'9')) && j > i {
350 j += 1;
351 continue;
352 }
353 }
354 break;
355 }
356 j - i
357}
358
359fn scan_multi_punct(bytes: &[u8], i: usize) -> usize {
360 let three = bytes
361 .get(i..i + 3)
362 .map(|s| std::str::from_utf8(s).unwrap_or(""))
363 .unwrap_or("");
364 let two = bytes
365 .get(i..i + 2)
366 .map(|s| std::str::from_utf8(s).unwrap_or(""))
367 .unwrap_or("");
368 if matches!(three, "<<=" | ">>=" | "..." | "->*") {
369 return 3;
370 }
371 if matches!(
372 two,
373 "->" | "::"
374 | "=="
375 | "!="
376 | "<="
377 | ">="
378 | "&&"
379 | "||"
380 | "<<"
381 | ">>"
382 | "+="
383 | "-="
384 | "*="
385 | "/="
386 | "%="
387 | "&="
388 | "|="
389 | "^="
390 | "++"
391 | "--"
392 | ".*"
393 ) {
394 return 2;
395 }
396 let c = char_at(unsafe { std::str::from_utf8_unchecked(bytes) }, i);
397 c.len_utf8()
398}
399
400fn peek(bytes: &[u8], i: usize) -> Option<u8> {
401 bytes.get(i).copied()
402}
403
404fn char_at(src: &str, i: usize) -> char {
405 src[i..].chars().next().unwrap_or('\0')
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 fn min_c(s: &str) -> String {
413 minify(s, &MinifyOptions::default(), false).unwrap().body
414 }
415 fn min_cpp(s: &str) -> String {
416 minify(s, &MinifyOptions::default(), true).unwrap().body
417 }
418
419 #[test]
420 fn c_basic() {
421 let src = "int main() {\n return 0;\n}\n";
422 assert_eq!(min_c(src), "int main(){return 0;}");
423 }
424
425 #[test]
426 fn c_preprocessor_kept_on_own_line() {
427 let src = "#include <stdio.h>\nint main() { return 0; }\n";
428 let out = min_c(src);
429 assert!(
430 out.starts_with("#include <stdio.h>\n"),
431 "preproc on own line: {:?}",
432 out
433 );
434 assert!(out.contains("int main(){return 0;}"));
435 }
436
437 #[test]
438 fn c_multiple_preprocessor_lines() {
439 let src = "#include <stdio.h>\n#include <stdlib.h>\nint x;\n";
440 let out = min_c(src);
441 assert_eq!(out, "#include <stdio.h>\n#include <stdlib.h>\nint x;");
442 }
443
444 #[test]
445 fn c_define_with_continuation() {
446 let src = "#define FOO(x) \\\n do { x; } while (0)\nint y = 1;\n";
447 let out = min_c(src);
448 assert!(out.starts_with("#define FOO(x) \\\n do { x; } while (0)\n"));
451 assert!(out.ends_with("int y=1;"));
452 }
453
454 #[test]
455 fn c_strips_line_comment() {
456 let src = "// hi\nint x;\n";
457 assert_eq!(min_c(src), "int x;");
458 }
459
460 #[test]
461 fn c_strips_block_comment() {
462 let src = "/* hi */ int x;\n";
463 assert_eq!(min_c(src), "int x;");
464 }
465
466 #[test]
467 fn cpp_template_double_close() {
468 let src = "vector<vector<int>> v;";
473 let out = min_cpp(src);
474 assert_eq!(out, "vector<vector<int>>v;");
475 }
476
477 #[test]
478 fn cpp_template_with_space_at_close() {
479 let src = "vector<vector<int> > v;";
483 let out = min_cpp(src);
484 assert!(out.contains("> >"), "got: {}", out);
485 }
486
487 #[test]
488 fn cpp_raw_string() {
489 let src = r#"const char* s = R"x(hi)x";"#;
490 let out = min_cpp(src);
491 assert!(out.contains(r#"R"x(hi)x""#), "got: {}", out);
492 }
493
494 #[test]
495 fn cpp_wide_string() {
496 let src = "const wchar_t* s = L\"hi\";";
497 let out = min_cpp(src);
498 assert!(out.contains("L\"hi\""));
499 }
500
501 #[test]
502 fn cpp_u8_string() {
503 let src = "const char* s = u8\"hi\";";
504 let out = min_cpp(src);
505 assert!(out.contains("u8\"hi\""));
506 }
507
508 #[test]
509 fn cpp_arrow_member() {
510 let src = "p->x = 1;";
511 let out = min_cpp(src);
512 assert_eq!(out, "p->x=1;");
513 }
514
515 #[test]
516 fn cpp_scope_resolution() {
517 let src = "std::string s;";
518 let out = min_cpp(src);
519 assert_eq!(out, "std::string s;");
520 }
521
522 #[test]
523 fn c_keep_comments() {
524 let src = "// hi\nint x;\n";
525 let r = minify(
526 src,
527 &MinifyOptions {
528 keep_comments: true,
529 },
530 false,
531 )
532 .unwrap();
533 assert!(r.body.starts_with("/* hi*/"));
534 assert_eq!(r.warnings.len(), 1);
535 }
536
537 #[test]
538 fn c_line_comment_continuation_stays_dead() {
539 let src = "int x = 1; // note \\\nint y = 2;\nint z = 3;\n";
543 let out = min_c(src);
544 assert!(!out.contains("int y=2;"), "dead code resurrected: {}", out);
545 assert!(out.contains("int x=1;"), "got: {}", out);
546 assert!(out.contains("int z=3;"), "got: {}", out);
547 }
548
549 #[test]
550 fn c_keep_comments_defuses_close_marker() {
551 let src = "// see */ marker\nint x;\n";
554 let r = minify(
555 src,
556 &MinifyOptions {
557 keep_comments: true,
558 },
559 false,
560 )
561 .unwrap();
562 let after_comment = r.body.split("*/").nth(1).unwrap_or("");
563 assert!(
564 !after_comment.contains("marker"),
565 "comment text leaked as live code: {}",
566 r.body
567 );
568 }
569
570 #[test]
571 fn c_unterminated_block_comment() {
572 assert!(minify("/* unterminated", &MinifyOptions::default(), false).is_err());
573 }
574
575 #[test]
576 fn c_unterminated_string() {
577 assert!(minify("char* s = \"oops", &MinifyOptions::default(), false).is_err());
578 }
579}