use js::token::{self, Keyword, 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> {
letter: char,
lower: Option<Box<VariableNameGenerator<'a>>>,
prepend: Option<&'a str>,
}
impl<'a> VariableNameGenerator<'a> {
fn new(prepend: Option<&'a str>, nb_letter: usize) -> VariableNameGenerator<'a> {
if nb_letter > 1 {
VariableNameGenerator {
letter: 'a',
lower: Some(Box::new(VariableNameGenerator::new(None, nb_letter - 1))),
prepend,
}
} else {
VariableNameGenerator {
letter: 'a',
lower: None,
prepend,
}
}
}
fn next(&mut self) {
self.incr_letters();
}
fn to_string(&self) -> String {
if let Some(ref lower) = self.lower {
format!("{}{}{}",
match self.prepend {
Some(ref p) => p,
None => "",
},
self.letter,
lower.to_string())
} else {
format!("{}{}",
match self.prepend {
Some(ref p) => p,
None => "",
},
self.letter)
}
}
#[allow(dead_code)]
fn len(&self) -> usize {
let first = match self.prepend {
Some(ref s) => s.len(),
None => 0,
} + 1;
first + match self.lower {
Some(ref s) => s.len(),
None => 0,
}
}
fn incr_letters(&mut self) {
let max = [('z', 'A'), ('Z', '0'), ('9', 'a')];
for (m, next) in &max {
if self.letter == *m {
self.letter = *next;
if self.letter == 'a' {
if let Some(ref mut lower) = self.lower {
lower.incr_letters();
} else {
self.lower = Some(Box::new(VariableNameGenerator::new(None, 1)));
}
}
return;
}
}
self.letter = ((self.letter as u8) + 1) as char;
}
}
fn get_variables_name<'a>(tokens: &'a Tokens<'a>) -> HashSet<&'a str> {
let mut ret = HashSet::new();
let mut check_var_name = false;
for token in tokens.iter() {
if check_var_name {
match token.get_other() {
Some(s) => {
ret.insert(s);
check_var_name = false;
}
None => {}
}
} else {
match token.get_keyword() {
Some(Keyword::Let) | Some(Keyword::Var) => {
check_var_name = true;
}
_ => {}
}
}
}
ret
}
#[inline]
fn aggregate_strings_inner<'a, 'b: 'a>(
mut tokens: Tokens<'a>,
separation_token: Option<Token<'b>>,
) -> 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: HashMap<&Token, String> = HashMap::with_capacity(100);
let mut var_gen = VariableNameGenerator::new(Some("r_"), 2);
let mut next_name = var_gen.to_string();
let all_variables = get_variables_name(&tokens);
while all_variables.contains(&next_name.as_str()) {
var_gen.next();
next_name = var_gen.to_string();
}
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);
let str_token = token.get_string().unwrap();
if x.len() > 1 && validated.get(token).is_none() {
let len = str_token.len();
if (x.len() + 2 ) * len > next_name.len() + str_token.len() + 6 + x.len() * next_name.len() {
validated.insert(token, next_name.clone());
var_gen.next();
next_name = var_gen.to_string();
while all_variables.contains(&next_name.as_str()) {
var_gen.next();
next_name = var_gen.to_string();
}
}
}
}
}
let mut ret = Vec::with_capacity(validated.len());
macro_rules! inner_loop {
($x:ident) => {{
let mut $x = $x.into_iter().collect::<Vec<_>>();
$x.sort_unstable_by(|a, b| a.1.cmp(&b.1));
$x
}}
}
for (token, var_name) in inner_loop!(validated) {
ret.push((var_name, strs.remove(&token).unwrap()));
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());
}
}
if let Some(token) = separation_token {
new_vars.push(token);
}
new_vars.append(&mut tokens.0);
Tokens(new_vars)
}
#[inline]
pub fn aggregate_strings<'a>(tokens: Tokens<'a>) -> Tokens<'a> {
aggregate_strings_inner(tokens, None)
}
#[inline]
pub fn aggregate_strings_with_separation<'a, 'b: 'a>(
tokens: Tokens<'a>,
separation_token: Token<'b>,
) -> Tokens<'a> {
aggregate_strings_inner(tokens, Some(separation_token))
}
#[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
}
#[inline]
pub fn clean_tokens_except<'a, F: Fn(&Token<'a>) -> bool>(
mut tokens: Tokens<'a>,
f: F
) -> Tokens<'a> {
tokens.0.retain(|c| {
let res = !c.is_comment() && {
if let Some(x) = c.get_char() {
!x.is_useless()
} else {
true
}
};
if !res {
!f(c)
} else {
res
}
});
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_ba=\"cake!\";var x=[r_aa,r_aa,\
\"another nice string\",r_ba,r_ba,r_aa,r_ba,r_ba,r_ba];";
let result = simple_minify(source).apply(aggregate_strings)
.apply(clean_tokens)
.to_string();
assert_eq!(result, expected_result);
}
#[test]
fn string_duplicates_variables_already_exist() {
let source = r#"var r_aa=1;var x = ["a nice string", "a nice string", "another nice string", "cake!",
"cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
let expected_result = "var r_ba=\"a nice string\";var r_ca=\"cake!\";\
var r_aa=1;var x=[r_ba,r_ba,\
\"another nice string\",r_ca,r_ca,r_ba,r_ca,r_ca,r_ca];";
let result = simple_minify(source).apply(aggregate_strings)
.apply(clean_tokens)
.to_string();
assert_eq!(result, expected_result);
}
#[test]
fn string_duplicates_with_separator() {
use self::token::ReservedChar;
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_ba=\"cake!\";\nvar x=[r_aa,r_aa,\
\"another nice string\",r_ba,r_ba,r_aa,r_ba,r_ba,r_ba];";
let result = simple_minify(source).apply(clean_tokens)
.apply(|f| {
aggregate_strings_with_separation(f, Token::Char(ReservedChar::Backline))
}).to_string();
assert_eq!(result, expected_result);
}
#[test]
fn clean_except() {
use self::token::ReservedChar;
let source = r#"var x = [1, 2, 3];
var y = "salut";
var z = "ok!";"#;
let expected = r#"var x=[1,2,3];
var y="salut";
var z="ok!";"#;
let result = simple_minify(source).apply(|f| {
clean_tokens_except(f, |c| {
c.get_char() != Some(ReservedChar::Backline)
})
}).to_string();
assert_eq!(result, expected);
}
#[test]
fn name_generator() {
let s = ::std::iter::repeat('a').take(36).collect::<String>();
let s = ::std::iter::repeat(s).take(20000)
.enumerate()
.map(|(pos, s)| format!("{}{}", s, pos))
.collect::<Vec<_>>();
let source = format!("var x = [{}];",
s.iter()
.map(|s| format!("\"{0}\",\"{0}\"", s))
.collect::<Vec<_>>()
.join(","));
let result = simple_minify(&source).apply(clean_tokens)
.apply(aggregate_strings)
.to_string();
assert!(result.find("var r_aaa=").is_some());
assert!(result.find("var r_ab=").unwrap() < result.find("var r_ba=").unwrap());
}
#[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);
}