124 lines
3.5 KiB
Rust
124 lines
3.5 KiB
Rust
use anyhow::{Context, Result};
|
|
|
|
/// Conservative JavaScript minification without token rewriting.
|
|
///
|
|
/// JavaScript `/` is context-sensitive (division vs. RegExp literal), so this
|
|
/// minifier never guesses. Outside strings/templates/comments it collapses
|
|
/// horizontal whitespace only until the first slash on a physical line; the
|
|
/// remainder of that line is copied verbatim. This still removes common
|
|
/// indentation/alignment whitespace while preserving RegExp bodies, ASI-sensitive
|
|
/// line terminators and comment text exactly.
|
|
pub fn minify(body: &[u8]) -> Result<Vec<u8>> {
|
|
let input = std::str::from_utf8(body).context("JavaScript is not UTF-8")?;
|
|
let mut out = String::with_capacity(input.len());
|
|
let mut chars = input.chars().peekable();
|
|
let mut quote: Option<char> = None;
|
|
let mut block_comment = false;
|
|
let mut line_comment = false;
|
|
let mut slash_sensitive = false;
|
|
let mut horizontal_space = false;
|
|
|
|
while let Some(ch) = chars.next() {
|
|
if line_comment {
|
|
out.push(ch);
|
|
if ch == '\n' || ch == '\r' {
|
|
line_comment = false;
|
|
slash_sensitive = false;
|
|
horizontal_space = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if block_comment {
|
|
out.push(ch);
|
|
if ch == '*' && chars.peek() == Some(&'/') {
|
|
chars.next();
|
|
out.push('/');
|
|
block_comment = false;
|
|
}
|
|
if ch == '\n' || ch == '\r' {
|
|
slash_sensitive = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if let Some(active) = quote {
|
|
out.push(ch);
|
|
if ch == '\\' {
|
|
if let Some(next) = chars.next() {
|
|
out.push(next);
|
|
}
|
|
} else if ch == active {
|
|
quote = None;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if slash_sensitive {
|
|
out.push(ch);
|
|
if ch == '\n' || ch == '\r' {
|
|
slash_sensitive = false;
|
|
horizontal_space = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if matches!(ch, '\'' | '"' | '`') {
|
|
if horizontal_space {
|
|
out.push(' ');
|
|
horizontal_space = false;
|
|
}
|
|
quote = Some(ch);
|
|
out.push(ch);
|
|
continue;
|
|
}
|
|
|
|
if ch == '/' {
|
|
if horizontal_space {
|
|
out.push(' ');
|
|
horizontal_space = false;
|
|
}
|
|
out.push(ch);
|
|
match chars.peek() {
|
|
Some(&'/') => {
|
|
chars.next();
|
|
out.push('/');
|
|
line_comment = true;
|
|
}
|
|
Some(&'*') => {
|
|
chars.next();
|
|
out.push('*');
|
|
block_comment = true;
|
|
}
|
|
_ => slash_sensitive = true,
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if matches!(ch, ' ' | '\t' | '\u{000b}' | '\u{000c}') {
|
|
horizontal_space = true;
|
|
continue;
|
|
}
|
|
|
|
if ch == '\r' || ch == '\n' {
|
|
horizontal_space = false;
|
|
slash_sensitive = false;
|
|
out.push(ch);
|
|
continue;
|
|
}
|
|
|
|
if horizontal_space {
|
|
out.push(' ');
|
|
horizontal_space = false;
|
|
}
|
|
out.push(ch);
|
|
}
|
|
|
|
let trimmed_len = out.trim_end_matches([' ', '\t']).len();
|
|
out.truncate(trimmed_len);
|
|
Ok(out.into_bytes())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
include!("js/tests.rs");
|