use js::token::{self, Token, Tokens};
use std::collections::{HashMap, HashSet};
#[inline]
pub fn minify(source: &str) -> String {
token::tokenize(source).apply(clean_tokens).to_string()
}
#[inline]
pub fn minify_and_replace_keywords<'a>(
source: &'a str,
keywords_to_replace: &'a [(token::Keyword, &str)],
) -> Tokens<'a> {
let mut v = token::tokenize(source).apply(clean_tokens);
for &(keyword, replacement) in keywords_to_replace {
for token in v.0.iter_mut() {
if match token.get_keyword() {
Some(ref k) => *k == keyword,
_ => false,
} {
*token = token::Token::Other(replacement);
}
}
}
v
}
struct VariableNameGenerator<'a> {
cur1: char,
cur2: char,
prepend: &'a str,
}
fn incr_letters(letters: &mut [&mut char]) {
let max = [('z', 'A'), ('Z', '0'), ('9', 'a')];
for (m, next) in &max {
if letters[0] == m {
*letters[0] = *next;
if *letters[0] == 'a' {
incr_letters(&mut letters[1..]);
}
return;
}
}
*letters[0] = ((*letters[0] as u8) + 1) as char;
}
impl<'a> VariableNameGenerator<'a> {
fn new(prepend: &'a str) -> VariableNameGenerator<'a> {
VariableNameGenerator {
cur1: 'a',
cur2: 'a',
prepend,
}
}
fn next(&mut self) {
incr_letters(&mut [&mut self.cur2, &mut self.cur1]);
}
fn to_string(&self) -> String {
format!("{}{}{}", self.prepend, self.cur1, self.cur2)
}
}
#[inline]
pub fn aggregate_strings<'a>(mut tokens: Tokens<'a>) -> Tokens<'a> {
let mut new_vars = Vec::with_capacity(50);
for (var_name, positions) in {
let mut strs: HashMap<&Token, Vec<usize>> = HashMap::with_capacity(1000);
let mut validated: HashSet<&Token> = HashSet::with_capacity(100);
for (pos, token) in tokens.iter().enumerate() {
if token.is_string() {
let x = strs.entry(token).or_insert_with(|| Vec::with_capacity(1));
x.push(pos);
if x.len() > 1 {
let len = token.get_string().unwrap().len();
if x.len() * len > 10 + x.len() * 4 {
validated.insert(token);
}
}
}
}
let mut var_gen = VariableNameGenerator::new("r_");
let mut ret = Vec::with_capacity(validated.len());
#[cfg(test)]
macro_rules! inner_loop {
($x:ident) => {{
let mut $x = $x.into_iter().collect::<Vec<_>>();
$x.sort();
$x
}}
}
#[cfg(not(test))]
macro_rules! inner_loop {
($x:ident) => {
$x.iter()
}
}
for v in inner_loop!(validated) {
let x = strs.remove(v).unwrap();
ret.push((var_gen.to_string(), x));
var_gen.next();
}
ret
} {
new_vars.push(Token::CreatedVar(format!("var {}={};", var_name, tokens[positions[0]])));
for pos in positions {
tokens.0[pos] = Token::CreatedVar(var_name.clone());
}
}
new_vars.append(&mut tokens.0);
Tokens(new_vars)
}
#[inline]
pub fn simple_minify<'a>(source: &'a str) -> Tokens<'a> {
token::tokenize(source)
}
#[inline]
pub fn clean_tokens<'a>(mut tokens: Tokens<'a>) -> Tokens<'a> {
tokens.0.retain(|c| {
!c.is_comment() && {
if let Some(x) = c.get_char() {
!x.is_useless()
} else {
true
}
}
});
tokens
}
#[test]
fn string_duplicates() {
let source = r#"var x = ["a nice string", "a nice string", "another nice string", "cake!",
"cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
let expected_result = "var r_aa=\"a nice string\";var r_ab=\"cake!\";var x=[r_aa,r_aa,\
\"another nice string\",r_ab,r_ab,r_aa,r_ab,r_ab,r_ab];";
let result = simple_minify(source).apply(clean_tokens).apply(aggregate_strings).to_string();
assert_eq!(result, expected_result);
}
#[test]
fn simple_quote() {
let source = r#"var x = "\\";"#;
let expected_result = r#"var x="\\";"#;
assert_eq!(minify(source), expected_result);
}
#[test]
fn js_minify_test() {
let source = r##"
var foo = "something";
var another_var = 2348323;
// who doesn't like comments?
/* and even longer comments?
like
on
a
lot
of
lines!
Fun!
*/
function far_away(x, y) {
var x2 = x + 4;
return x * x2 + y;
}
// this call is useless
far_away(another_var, 12);
// this call is useless too
far_away(another_var, 12);
"##;
let expected_result = "var foo=\"something\";var another_var=2348323;function far_away(x,y){\
var x2=x+4;return x*x2+y;}far_away(another_var,12);far_away(another_var,\
12);";
assert_eq!(minify(source), expected_result);
}
#[test]
fn another_js_test() {
let source = r#"
/*! let's keep this license
*
* because everyone likes licenses!
*
* right?
*/
function forEach(data, func) {
for (var i = 0; i < data.length; ++i) {
func(data[i]);
}
}
forEach([0, 1, 2, 3, 4,
5, 6, 7, 8, 9], function (x) {
console.log(x);
});
// I think we're done?
console.log('done!');
"#;
let expected_result = r#"/*! let's keep this license
*
* because everyone likes licenses!
*
* right?
*/function forEach(data,func){for(var i=0;i<data.length;++i){func(data[i]);}}forEach([0,1,2,3,4,5,6,7,8,9],function(x){console.log(x);});console.log('done!');"#;
assert_eq!(minify(source), expected_result);
}
#[test]
fn comment_issue() {
let source = r#"
search_input.onchange = function(e) {
// Do NOT e.preventDefault() here. It will prevent pasting.
clearTimeout(searchTimeout);
// zero-timeout necessary here because at the time of event handler execution the
// pasted content is not in the input field yet. Shouldn’t make any difference for
// change, though.
setTimeout(search, 0);
};
"#;
let expected_result = "search_input.onchange=function(e){clearTimeout(searchTimeout);\
setTimeout(search,0);};";
assert_eq!(minify(source), expected_result);
}
#[test]
fn missing_whitespace() {
let source = r#"
for (var entry in results) {
if (results.hasOwnProperty(entry)) {
ar.push(results[entry]);
}
}"#;
let expected_result = "for(var entry in results){if(results.hasOwnProperty(entry)){\
ar.push(results[entry]);}}";
assert_eq!(minify(source), expected_result);
}
#[test]
fn weird_regex_issue() {
let source = r#"
val = val.replace(/\_/g, "");
var valGenerics = extractGenerics(val);"#;
let expected_result = "val=val.replace(/\\_/g,\"\");var valGenerics=extractGenerics(val);";
assert_eq!(minify(source), expected_result);
}
#[test]
fn replace_keyword() {
let source = r#"
var x = ['a', 'b', null, 'd', {'x': null, 'e': null, 'z': 'w'}];
var n = null;
"#;
let expected_result = "var x=['a','b',N,'d',{'x':N,'e':N,'z':'w'}];var n=N;";
assert_eq!(minify_and_replace_keywords(source, &[(token::Keyword::Null, "N")]).to_string(),
expected_result);
}