1 // MIT License
2 //
3 // Copyright (c) 2018 Guillaume Gomez
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the "Software"), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included in all
13 // copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 // SOFTWARE.
22 
23 use js::token::{self, Keyword, ReservedChar, Token, Tokens};
24 use js::utils::{get_array, get_variable_name_and_value_positions, VariableNameGenerator};
25 
26 use std::collections::{HashMap, HashSet};
27 
28 /*#[derive(Debug, Clone, PartialEq, Eq)]
29 enum Elem<'a> {
30     Function(Function<'a>),
31     Block(Block<'a>),
32     Variable(Variable<'a>),
33     Condition(token::Condition),
34     Loop(Loop<'a>),
35     Operation(Operation<'a>),
36 }
37 
38 impl<'a> Elem<'a> {
39     fn is_condition(&self) -> bool {
40         match *self {
41             Elem::Condition(_) => true,
42             _ => false,
43         }
44     }
45 }
46 
47 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
48 enum ConditionType {
49     If,
50     ElseIf,
51     Else,
52     Ternary,
53 }
54 
55 #[derive(Clone, PartialEq, Eq, Debug)]
56 struct Block<'a> {
57     elems: Vec<Elem<'a>>,
58 }
59 
60 #[derive(Clone, PartialEq, Eq, Debug)]
61 struct Argument<'a> {
62     name: &'a str,
63 }
64 
65 #[derive(Clone, PartialEq, Eq, Debug)]
66 struct Function<'a> {
67     name: Option<&'a str>,
68     args: Vec<Argument<'a>>,
69     block: Block<'a>,
70 }
71 
72 #[derive(Clone, PartialEq, Eq, Debug)]
73 struct Variable<'a> {
74     name: &'a str,
75     value: Option<&'a str>,
76 }
77 
78 /*struct Condition<'a> {
79     ty_: ConditionType,
80     condition: &'a str,
81     block: Block<'a>,
82 }*/
83 
84 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
85 enum LoopType {
86     Do,
87     For,
88     While,
89 }
90 
91 #[derive(Clone, PartialEq, Eq, Debug)]
92 struct Loop<'a> {
93     ty_: LoopType,
94     condition: Vec<Elem<'a>>,
95     block: Block<'a>,
96 }
97 
98 #[derive(Clone, PartialEq, Eq, Debug)]
99 struct Operation<'a> {
100     content: &'a str,
101 }
102 
103 fn get_while_condition<'a>(tokens: &[token::Token<'a>], pos: &mut usize) -> Result<Vec<Elem<'a>>, String> {
104     let tmp = *pos;
105     *pos += 1;
106     if let Err(e) = match tokens.get(tmp) {
107         Some(token::Token::Char(token::ReservedChar::OpenParenthese)) => Ok(()),
108         Some(e) => Err(format!("Expected \"(\", found \"{:?}\"", e)),
109         None => Err("Expected \"(\", found nothing...".to_owned()),
110     } {
111         return Err(e);
112     }
113     let mut elems: Vec<Elem<'a>> = Vec::with_capacity(1);
114 
115     while let Some(e) = tokens.get(*pos) {
116         *pos += 1;
117         match e {
118             token::Token::Char(token::ReservedChar::CloseParenthese) => return Ok(elems),
119             token::Token::Condition(e) => {
120                 if let Some(cond) = elems.last() {
121                     if cond.is_condition() {
122                         return Err(format!("\"{:?}\" cannot follow \"{:?}\"", e, cond));
123                     }
124                 }
125             }
126             _ => {}
127         }
128     }
129     Err("Expected \")\", found nothing...".to_owned())
130 }
131 
132 fn get_do<'a>(tokens: &[token::Token<'a>], pos: &mut usize) -> Result<Elem<'a>, String> {
133     let tmp = *pos;
134     *pos += 1;
135     let block = match tokens.get(tmp) {
136         Some(token::Token::Char(token::ReservedChar::OpenCurlyBrace)) => get_block(tokens, pos, true),
137         Some(e) => Err(format!("Expected \"{{\", found \"{:?}\"", e)),
138         None => Err("Expected \"{\", found nothing...".to_owned()),
139     }?;
140     let tmp = *pos;
141     *pos += 1;
142     let condition = match tokens.get(tmp) {
143         Some(token::Token::Keyword(token::Keyword::While)) => get_while_condition(tokens, pos),
144         Some(e) => Err(format!("Expected \"while\", found \"{:?}\"", e)),
145         None => Err("Expected \"while\", found nothing...".to_owned()),
146     }?;
147     let mut loop_ = Loop {
148         ty_: LoopType::Do,
149         condition: condition,
150         block,
151     };
152     Ok(Elem::Loop(loop_))
153 }
154 
155 fn get_block<'a>(tokens: &[token::Token<'a>], pos: &mut usize,
156                  start_with_paren: bool) -> Result<Block<'a>, String> {
157     let mut block = Block { elems: Vec::with_capacity(2) };
158     while let Some(e) = tokens.get(*pos) {
159         *pos += 1;
160         block.elems.push(match e {
161             token::Token::Keyword(token::Keyword::Do) => get_do(tokens, pos),
162             token::Token::Char(token::ReservedChar::CloseCurlyBrace) => {
163                 if start_with_paren {
164                     return Ok(block);
165                 }
166                 return Err("Unexpected \"}\"".to_owned());
167             }
168         }?);
169     }
170     if !start_with_paren {
171         Ok(block)
172     } else {
173         Err("Expected \"}\" at the end of the block but didn't find one...".to_owned())
174     }
175 }
176 
177 fn build_ast<'a>(v: &[token::Token<'a>]) -> Result<Elem<'a>, String> {
178     let mut pos = 0;
179 
180     match get_block(v, &mut pos, false) {
181         Ok(ast) => Ok(Elem::Block(ast)),
182         Err(e) => Err(e),
183     }
184 }*/
185 
186 /// Minifies a given JS source code.
187 ///
188 /// # Example
189 ///
190 /// ```rust
191 /// extern crate minifier;
192 /// use minifier::js::minify;
193 ///
194 /// fn main() {
195 ///     let js = r#"
196 ///         function forEach(data, func) {
197 ///            for (var i = 0; i < data.length; ++i) {
198 ///                func(data[i]);
199 ///            }
200 ///         }"#.into();
201 ///     let js_minified = minify(js);
202 /// }
203 /// ```
204 #[inline]
minify(source: &str) -> String205 pub fn minify(source: &str) -> String {
206     token::tokenize(source)
207         .apply(::js::clean_tokens)
208         .to_string()
209 }
210 
211 // TODO: No scope handling or anything. Might be nice as a second step to add it...
get_variables_name<'a>( tokens: &'a Tokens<'a>, ) -> (HashSet<&'a str>, HashMap<&'a str, (usize, usize)>)212 fn get_variables_name<'a>(
213     tokens: &'a Tokens<'a>,
214 ) -> (HashSet<&'a str>, HashMap<&'a str, (usize, usize)>) {
215     let mut ret = HashSet::new();
216     let mut variables = HashMap::new();
217     let mut pos = 0;
218 
219     while pos < tokens.len() {
220         if tokens[pos].is_keyword() || tokens[pos].is_other() {
221             if let Some((var_pos, Some(value_pos))) =
222                 get_variable_name_and_value_positions(tokens, pos)
223             {
224                 pos = value_pos;
225                 if let Some(var_name) = tokens[var_pos].get_other() {
226                     if !var_name.starts_with("r_") {
227                         pos += 1;
228                         continue;
229                     }
230                     ret.insert(var_name);
231                 }
232                 if let Some(s) = tokens[value_pos].get_string() {
233                     variables.insert(s, (var_pos, value_pos));
234                 }
235             }
236         }
237         pos += 1;
238     }
239     (ret, variables)
240 }
241 
242 #[inline]
aggregate_strings_inner<'a, 'b: 'a>( mut tokens: Tokens<'a>, separation_token: Option<Token<'b>>, ) -> Tokens<'a>243 fn aggregate_strings_inner<'a, 'b: 'a>(
244     mut tokens: Tokens<'a>,
245     separation_token: Option<Token<'b>>,
246 ) -> Tokens<'a> {
247     let mut new_vars = Vec::with_capacity(50);
248     let mut to_replace: Vec<(usize, usize)> = Vec::new();
249 
250     for (var_name, positions) in {
251         let mut strs: HashMap<&Token, Vec<usize>> = HashMap::with_capacity(1000);
252         let mut validated: HashMap<&Token, String> = HashMap::with_capacity(100);
253 
254         let mut var_gen = VariableNameGenerator::new(Some("r_"), 2);
255         let mut next_name = var_gen.to_string();
256 
257         let (all_variables, values) = get_variables_name(&tokens);
258         while all_variables.contains(&next_name.as_str()) {
259             var_gen.next();
260             next_name = var_gen.to_string();
261         }
262 
263         for pos in 0..tokens.len() {
264             let token = &tokens[pos];
265             if let Some(str_token) = token.get_string() {
266                 if let Some((var_pos, string_pos)) = values.get(&str_token) {
267                     if pos != *string_pos {
268                         to_replace.push((pos, *var_pos));
269                     }
270                     continue;
271                 }
272                 let x = strs.entry(token).or_insert_with(|| Vec::with_capacity(1));
273                 x.push(pos);
274                 if x.len() > 1 && validated.get(token).is_none() {
275                     let len = str_token.len();
276                     // Computation here is simple, we declare new variables when creating this so
277                     // the total of characters must be shorter than:
278                     // `var r_aa=...;` -> 10 + `r_aa` -> 14
279                     if (x.len() + 2/* quotes */) * len
280                         > next_name.len() + str_token.len() + 6 /* var _=_;*/ + x.len() * next_name.len()
281                     {
282                         validated.insert(token, next_name.clone());
283                         var_gen.next();
284                         next_name = var_gen.to_string();
285                         while all_variables.contains(&next_name.as_str()) {
286                             var_gen.next();
287                             next_name = var_gen.to_string();
288                         }
289                     }
290                 }
291             }
292         }
293         let mut ret = Vec::with_capacity(validated.len());
294 
295         // We need this macro to avoid having to sort the set when not testing the crate.
296         //#[cfg(test)]
297         macro_rules! inner_loop {
298             ($x:ident) => {{
299                 let mut $x = $x.into_iter().collect::<Vec<_>>();
300                 $x.sort_unstable_by(|a, b| a.1.cmp(&b.1));
301                 $x
302             }};
303         }
304         /*#[cfg(not(test))]
305         macro_rules! inner_loop {
306             ($x:ident) => {
307                 $x.into_iter()
308             }
309         }*/
310 
311         for (token, var_name) in inner_loop!(validated) {
312             ret.push((var_name, strs.remove(&token).unwrap()));
313             var_gen.next();
314         }
315         ret
316     } {
317         if new_vars.is_empty() {
318             new_vars.push(Token::Keyword(Keyword::Var));
319         } else {
320             new_vars.push(Token::Char(ReservedChar::Comma));
321         }
322         new_vars.push(Token::CreatedVarDecl(format!(
323             "{}={}",
324             var_name, tokens[positions[0]]
325         )));
326         for pos in positions {
327             tokens.0[pos] = Token::CreatedVar(var_name.clone());
328         }
329     }
330     if !new_vars.is_empty() {
331         new_vars.push(Token::Char(ReservedChar::SemiColon));
332     }
333     for (to_replace_pos, variable_pos) in to_replace {
334         tokens.0[to_replace_pos] = tokens.0[variable_pos].clone();
335     }
336     if let Some(token) = separation_token {
337         new_vars.push(token);
338     }
339     new_vars.append(&mut tokens.0);
340     Tokens(new_vars)
341 }
342 
343 /// Aggregate litteral strings. For instance, if the string litteral "Oh look over there!"
344 /// appears more than once, a variable will be created with this value and used everywhere the
345 /// string appears. Of course, this replacement is only performed when it allows to take
346 /// less space.
347 ///
348 /// # Example
349 ///
350 /// ```rust,no_run
351 /// extern crate minifier;
352 /// use minifier::js::{aggregate_strings, clean_tokens, simple_minify};
353 /// use std::fs;
354 ///
355 /// fn main() {
356 ///     let content = fs::read("some_file.js").expect("file not found");
357 ///     let source = String::from_utf8_lossy(&content);
358 ///     let s = simple_minify(&source);    // First we get the tokens list.
359 ///     let s = s.apply(aggregate_strings) // This `apply` aggregates string litterals.
360 ///              .apply(clean_tokens)      // This one is used to remove useless chars.
361 ///              .to_string();             // And we finally convert to string.
362 ///     println!("result: {}", s);
363 /// }
364 /// ```
365 #[inline]
aggregate_strings(tokens: Tokens<'_>) -> Tokens<'_>366 pub fn aggregate_strings(tokens: Tokens<'_>) -> Tokens<'_> {
367     aggregate_strings_inner(tokens, None)
368 }
369 
370 /// Exactly like `aggregate_strings` except this one expects a separation token
371 /// to be passed. This token will be placed between the created variables for the
372 /// strings aggregation and the rest.
373 ///
374 /// # Example
375 ///
376 /// Let's add a backline between the created variables and the rest of the code:
377 ///
378 /// ```rust,no_run
379 /// extern crate minifier;
380 /// use minifier::js::{
381 ///     aggregate_strings_with_separation,
382 ///     clean_tokens,
383 ///     simple_minify,
384 ///     Token,
385 ///     ReservedChar,
386 /// };
387 /// use std::fs;
388 ///
389 /// fn main() {
390 ///     let content = fs::read("some_file.js").expect("file not found");
391 ///     let source = String::from_utf8_lossy(&content);
392 ///     let s = simple_minify(&source);    // First we get the tokens list.
393 ///     let s = s.apply(|f| {
394 ///                  aggregate_strings_with_separation(f, Token::Char(ReservedChar::Backline))
395 ///              })                   // We add a backline between the variable and the rest.
396 ///              .apply(clean_tokens) // We clean the tokens.
397 ///              .to_string();        // And we finally convert to string.
398 ///     println!("result: {}", s);
399 /// }
400 /// ```
401 #[inline]
aggregate_strings_with_separation<'a, 'b: 'a>( tokens: Tokens<'a>, separation_token: Token<'b>, ) -> Tokens<'a>402 pub fn aggregate_strings_with_separation<'a, 'b: 'a>(
403     tokens: Tokens<'a>,
404     separation_token: Token<'b>,
405 ) -> Tokens<'a> {
406     aggregate_strings_inner(tokens, Some(separation_token))
407 }
408 
409 #[inline]
aggregate_strings_into_array_inner<'a, 'b: 'a, T: Fn(&Tokens<'a>, usize) -> bool>( mut tokens: Tokens<'a>, array_name: &str, separation_token: Option<Token<'b>>, filter: T, ) -> Tokens<'a>410 fn aggregate_strings_into_array_inner<'a, 'b: 'a, T: Fn(&Tokens<'a>, usize) -> bool>(
411     mut tokens: Tokens<'a>,
412     array_name: &str,
413     separation_token: Option<Token<'b>>,
414     filter: T,
415 ) -> Tokens<'a> {
416     let mut to_insert = Vec::with_capacity(100);
417     let mut to_replace = Vec::with_capacity(100);
418 
419     {
420         let mut to_ignore = HashSet::new();
421         // key: the token string
422         // value: (position in the array, positions in the tokens list, need creation)
423         let mut strs: HashMap<&str, (usize, Vec<usize>, bool)> = HashMap::with_capacity(1000);
424         let (current_array_values, need_recreate, mut end_bracket) =
425             match get_array(&tokens, array_name) {
426                 Some((s, p)) => (s, false, p),
427                 None => (Vec::new(), true, 0),
428             };
429         let mut validated: HashSet<&str> = HashSet::new();
430 
431         let mut array_pos = 0;
432         let mut array_pos_str;
433         for s in current_array_values.iter() {
434             if let Some(st) = tokens.0[*s].get_string() {
435                 strs.insert(&st[1..st.len() - 1], (array_pos, vec![], false));
436                 array_pos += 1;
437                 validated.insert(&st[1..st.len() - 1]);
438                 to_ignore.insert(*s);
439             }
440         }
441 
442         array_pos_str = array_pos.to_string();
443         for pos in 0..tokens.len() {
444             if to_ignore.contains(&pos) {
445                 continue;
446             }
447             let token = &tokens[pos];
448             if let Some(str_token) = token.get_string() {
449                 if !filter(&tokens, pos) {
450                     continue;
451                 }
452                 let s = &str_token[1..str_token.len() - 1];
453                 let x = strs
454                     .entry(s)
455                     .or_insert_with(|| (0, Vec::with_capacity(1), true));
456                 x.1.push(pos);
457                 if x.1.len() > 1 && !validated.contains(s) {
458                     let len = s.len();
459                     if len * x.1.len()
460                         > (array_name.len() + array_pos_str.len() + 2) * x.1.len()
461                             + array_pos_str.len()
462                             + 2
463                     {
464                         validated.insert(&str_token[1..str_token.len() - 1]);
465                         x.0 = array_pos;
466                         array_pos += 1;
467                         array_pos_str = array_pos.to_string();
468                     }
469                 }
470             }
471         }
472 
473         // TODO:
474         // 1. Sort strings by length (the smallest should take the smallest numbers
475         //    for bigger gains).
476         // 2. Compute "score" for all strings of the same length and sort the strings
477         //    of the same length with this score.
478         // 3. Loop again over strings and remove those who shouldn't be there anymore.
479         // 4. Repeat.
480         //
481         // ALTERNATIVE:
482         //
483         // Compute the score based on:
484         // current number of digits * str length * str occurence
485         //
486         // ^ This second solution should bring even better results.
487         //
488         // ALSO: if an array with such strings already exists, it'd be worth it to recompute
489         // everything again.
490         let mut validated = validated.iter().map(|v| (strs[v].0, v)).collect::<Vec<_>>();
491         validated.sort_unstable_by(|(p1, _), (p2, _)| p2.cmp(p1));
492 
493         if need_recreate && !validated.is_empty() {
494             if let Some(token) = separation_token {
495                 to_insert.push((0, token));
496             }
497             to_insert.push((0, Token::Char(ReservedChar::SemiColon)));
498             to_insert.push((0, Token::Char(ReservedChar::CloseBracket)));
499             to_insert.push((0, Token::Char(ReservedChar::OpenBracket)));
500             to_insert.push((0, Token::CreatedVarDecl(format!("var {}=", array_name))));
501 
502             end_bracket = 2;
503         }
504 
505         let mut iter = validated.iter().peekable();
506         while let Some((array_pos, s)) = iter.next() {
507             let (_, ref tokens_pos, create_array_entry) = strs[*s];
508             let array_index = Token::CreatedVar(format!("{}[{}]", array_name, array_pos));
509             for token in tokens_pos.iter() {
510                 to_replace.push((*token, array_index.clone()));
511             }
512             if !create_array_entry {
513                 continue;
514             }
515             to_insert.push((end_bracket, Token::CreatedVar(format!("\"{}\"", *s))));
516             if iter.peek().is_none() && current_array_values.is_empty() {
517                 continue;
518             }
519             to_insert.push((end_bracket, Token::Char(ReservedChar::Comma)));
520         }
521     }
522     for (pos, rep) in to_replace.into_iter() {
523         tokens.0[pos] = rep;
524     }
525     for (pos, rep) in to_insert.into_iter() {
526         tokens.0.insert(pos, rep);
527     }
528     tokens
529 }
530 
531 /// Exactly like `aggregate_strings_into_array` except this one expects a separation token
532 /// to be passed. This token will be placed between the created array for the
533 /// strings aggregation and the rest.
534 ///
535 /// # Example
536 ///
537 /// Let's add a backline between the created variables and the rest of the code:
538 ///
539 /// ```rust,no_run
540 /// extern crate minifier;
541 /// use minifier::js::{
542 ///     aggregate_strings_into_array_with_separation,
543 ///     clean_tokens,
544 ///     simple_minify,
545 ///     Token,
546 ///     ReservedChar,
547 /// };
548 /// use std::fs;
549 ///
550 /// fn main() {
551 ///     let content = fs::read("some_file.js").expect("file not found");
552 ///     let source = String::from_utf8_lossy(&content);
553 ///     let s = simple_minify(&source);    // First we get the tokens list.
554 ///     let s = s.apply(|f| {
555 ///                  aggregate_strings_into_array_with_separation(f, "R", Token::Char(ReservedChar::Backline))
556 ///              })                   // We add a backline between the variable and the rest.
557 ///              .apply(clean_tokens) // We clean the tokens.
558 ///              .to_string();        // And we finally convert to string.
559 ///     println!("result: {}", s);
560 /// }
561 /// ```
562 #[inline]
aggregate_strings_into_array_with_separation<'a, 'b: 'a>( tokens: Tokens<'a>, array_name: &str, separation_token: Token<'b>, ) -> Tokens<'a>563 pub fn aggregate_strings_into_array_with_separation<'a, 'b: 'a>(
564     tokens: Tokens<'a>,
565     array_name: &str,
566     separation_token: Token<'b>,
567 ) -> Tokens<'a> {
568     aggregate_strings_into_array_inner(tokens, array_name, Some(separation_token), |_, _| true)
569 }
570 
571 /// Same as [`aggregate_strings_into_array_with_separation`] except it allows certain strings to
572 /// not be aggregated thanks to the `filter` parameter. If it returns `false`, then the string will
573 /// be ignored.
574 #[inline]
aggregate_strings_into_array_with_separation_filter<'a, 'b: 'a, T>( tokens: Tokens<'a>, array_name: &str, separation_token: Token<'b>, filter: T, ) -> Tokens<'a> where T: Fn(&Tokens<'a>, usize) -> bool,575 pub fn aggregate_strings_into_array_with_separation_filter<'a, 'b: 'a, T>(
576     tokens: Tokens<'a>,
577     array_name: &str,
578     separation_token: Token<'b>,
579     filter: T,
580 ) -> Tokens<'a>
581 where
582     T: Fn(&Tokens<'a>, usize) -> bool,
583 {
584     aggregate_strings_into_array_inner(tokens, array_name, Some(separation_token), filter)
585 }
586 
587 /// Aggregate litteral strings. For instance, if the string litteral "Oh look over there!"
588 /// appears more than once, it will be added to the generated array and used everywhere the
589 /// string appears. Of course, this replacement is only performed when it allows to take
590 /// less space.
591 ///
592 /// # Example
593 ///
594 /// ```rust,no_run
595 /// extern crate minifier;
596 /// use minifier::js::{aggregate_strings_into_array, clean_tokens, simple_minify};
597 /// use std::fs;
598 ///
599 /// fn main() {
600 ///     let content = fs::read("some_file.js").expect("file not found");
601 ///     let source = String::from_utf8_lossy(&content);
602 ///     let s = simple_minify(&source);    // First we get the tokens list.
603 ///     let s = s.apply(|f| aggregate_strings_into_array(f, "R")) // This `apply` aggregates string litterals.
604 ///              .apply(clean_tokens)      // This one is used to remove useless chars.
605 ///              .to_string();             // And we finally convert to string.
606 ///     println!("result: {}", s);
607 /// }
608 /// ```
609 #[inline]
aggregate_strings_into_array<'a>(tokens: Tokens<'a>, array_name: &str) -> Tokens<'a>610 pub fn aggregate_strings_into_array<'a>(tokens: Tokens<'a>, array_name: &str) -> Tokens<'a> {
611     aggregate_strings_into_array_inner(tokens, array_name, None, |_, _| true)
612 }
613 
614 /// Same as [`aggregate_strings_into_array`] except it allows certain strings to not be aggregated
615 /// thanks to the `filter` parameter. If it returns `false`, then the string will be ignored.
616 #[inline]
aggregate_strings_into_array_filter<'a, T>( tokens: Tokens<'a>, array_name: &str, filter: T, ) -> Tokens<'a> where T: Fn(&Tokens<'a>, usize) -> bool,617 pub fn aggregate_strings_into_array_filter<'a, T>(
618     tokens: Tokens<'a>,
619     array_name: &str,
620     filter: T,
621 ) -> Tokens<'a>
622 where
623     T: Fn(&Tokens<'a>, usize) -> bool,
624 {
625     aggregate_strings_into_array_inner(tokens, array_name, None, filter)
626 }
627 
628 /// Simple function to get the untouched token list. Useful in case you want to perform some
629 /// actions directly on it.
630 ///
631 /// # Example
632 ///
633 /// ```rust,no_run
634 /// extern crate minifier;
635 /// use minifier::js::simple_minify;
636 /// use std::fs;
637 ///
638 /// fn main() {
639 ///     let content = fs::read("some_file.js").expect("file not found");
640 ///     let source = String::from_utf8_lossy(&content);
641 ///     let s = simple_minify(&source);
642 ///     println!("result: {:?}", s); // We now have the tokens list.
643 /// }
644 /// ```
645 #[inline]
simple_minify(source: &str) -> Tokens<'_>646 pub fn simple_minify(source: &str) -> Tokens<'_> {
647     token::tokenize(source)
648 }
649 
650 #[test]
aggregate_strings_in_array()651 fn aggregate_strings_in_array() {
652     let source = r#"var x = ["a nice string", "a nice string", "another nice string", "cake!",
653                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
654     let expected_result = "var R=[\"a nice string\",\"cake!\"];var x=[R[0],R[0],\
655                            \"another nice string\",R[1],R[1],R[0],R[1],R[1],R[1]]";
656 
657     let result = simple_minify(source)
658         .apply(::js::clean_tokens)
659         .apply(|c| aggregate_strings_into_array(c, "R"))
660         .to_string();
661     assert_eq!(result, expected_result);
662 
663     let source = r#"var x = ["a nice string", "a nice string", "another nice string", "cake!",
664                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
665     let expected_result = "var R=[\"a nice string\",\"cake!\"];\nvar x=[R[0],R[0],\
666                            \"another nice string\",R[1],R[1],R[0],R[1],R[1],R[1]]";
667 
668     let result = simple_minify(source)
669         .apply(::js::clean_tokens)
670         .apply(|c| {
671             aggregate_strings_into_array_with_separation(
672                 c,
673                 "R",
674                 Token::Char(ReservedChar::Backline),
675             )
676         })
677         .to_string();
678     assert_eq!(result, expected_result);
679 
680     let source = r#"var x = ["a nice string", "a nice string", "another nice string", "another nice string", "another nice string", "another nice string","cake!","cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
681     let expected_result = "var R=[\"a nice string\",\"another nice string\",\"cake!\"];\n\
682                            var x=[R[0],R[0],R[1],R[1],R[1],R[1],R[2],R[2],R[0],R[2],\
683                            R[2],R[2]]";
684 
685     let result = simple_minify(source)
686         .apply(::js::clean_tokens)
687         .apply(|c| {
688             aggregate_strings_into_array_with_separation(
689                 c,
690                 "R",
691                 Token::Char(ReservedChar::Backline),
692             )
693         })
694         .to_string();
695     assert_eq!(result, expected_result);
696 }
697 
698 #[test]
aggregate_strings_in_array_filter()699 fn aggregate_strings_in_array_filter() {
700     let source = r#"var searchIndex = {};searchIndex['duplicate_paths'] = {'aaaaaaaa': 'bbbbbbbb', 'bbbbbbbb': 'aaaaaaaa', 'duplicate_paths': 'aaaaaaaa'};"#;
701     let expected_result = "var R=[\"bbbbbbbb\",\"aaaaaaaa\"];\nvar searchIndex={};searchIndex['duplicate_paths']={R[1]:R[0],R[0]:R[1],'duplicate_paths':R[1]}";
702 
703     let result = simple_minify(source)
704         .apply(::js::clean_tokens)
705         .apply(|c| {
706             aggregate_strings_into_array_with_separation_filter(
707                 c,
708                 "R",
709                 Token::Char(ReservedChar::Backline),
710                 |tokens, pos| {
711                     pos < 2
712                         || !tokens[pos - 1].eq_char(ReservedChar::OpenBracket)
713                         || tokens[pos - 2].get_other() != Some("searchIndex")
714                 },
715             )
716         })
717         .to_string();
718     assert_eq!(result, expected_result);
719 
720     let source = r#"var searchIndex = {};searchIndex['duplicate_paths'] = {'aaaaaaaa': 'bbbbbbbb', 'bbbbbbbb': 'aaaaaaaa', 'duplicate_paths': 'aaaaaaaa', 'x': 'duplicate_paths'};"#;
721     let expected_result = "var R=[\"bbbbbbbb\",\"aaaaaaaa\",\"duplicate_paths\"];\nvar searchIndex={};searchIndex['duplicate_paths']={R[1]:R[0],R[0]:R[1],R[2]:R[1],'x':R[2]}";
722 
723     let result = simple_minify(source)
724         .apply(::js::clean_tokens)
725         .apply(|c| {
726             aggregate_strings_into_array_with_separation_filter(
727                 c,
728                 "R",
729                 Token::Char(ReservedChar::Backline),
730                 |tokens, pos| {
731                     pos < 2
732                         || !tokens[pos - 1].eq_char(ReservedChar::OpenBracket)
733                         || tokens[pos - 2].get_other() != Some("searchIndex")
734                 },
735             )
736         })
737         .to_string();
738     assert_eq!(result, expected_result);
739 }
740 
741 #[test]
aggregate_strings_in_array_existing()742 fn aggregate_strings_in_array_existing() {
743     let source = r#"var R=[];var x = ["a nice string", "a nice string", "another nice string", "cake!",
744                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
745     let expected_result = "var R=[\"a nice string\",\"cake!\"];var x=[R[0],R[0],\
746                            \"another nice string\",R[1],R[1],R[0],R[1],R[1],R[1]]";
747 
748     let result = simple_minify(source)
749         .apply(::js::clean_tokens)
750         .apply(|c| aggregate_strings_into_array(c, "R"))
751         .to_string();
752     assert_eq!(result, expected_result);
753 
754     let source = r#"var R=["a nice string"];var x = ["a nice string", "a nice string", "another nice string", "cake!",
755                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
756     let expected_result = "var R=[\"a nice string\",\"cake!\"];var x=[R[0],R[0],\
757                            \"another nice string\",R[1],R[1],R[0],R[1],R[1],R[1]]";
758 
759     let result = simple_minify(source)
760         .apply(::js::clean_tokens)
761         .apply(|c| aggregate_strings_into_array(c, "R"))
762         .to_string();
763     assert_eq!(result, expected_result);
764 
765     let source = r#"var y = 12;var R=["a nice string"];var x = ["a nice string", "a nice string", "another nice string", "cake!",
766                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
767     let expected_result = "var y=12;var R=[\"a nice string\",\"cake!\"];var x=[R[0],R[0],\
768                            \"another nice string\",R[1],R[1],R[0],R[1],R[1],R[1]]";
769 
770     let result = simple_minify(source)
771         .apply(::js::clean_tokens)
772         .apply(|c| aggregate_strings_into_array(c, "R"))
773         .to_string();
774     assert_eq!(result, expected_result);
775 
776     let source = r#"var R=["osef1", "o2", "damn"];
777                     var x = ["a nice string", "a nice string", "another nice string", "cake!",
778                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
779     let expected_result = "var R=[\"osef1\",\"o2\",\"damn\",\"a nice string\",\"cake!\"];\
780                            var x=[R[3],R[3],\"another nice string\",R[4],R[4],R[3],R[4],R[4],R[4]]";
781 
782     let result = simple_minify(source)
783         .apply(::js::clean_tokens)
784         .apply(|c| aggregate_strings_into_array(c, "R"))
785         .to_string();
786     assert_eq!(result, expected_result);
787 }
788 
789 #[test]
string_duplicates()790 fn string_duplicates() {
791     let source = r#"var x = ["a nice string", "a nice string", "another nice string", "cake!",
792                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
793     let expected_result = "var r_aa=\"a nice string\",r_ba=\"cake!\";var x=[r_aa,r_aa,\
794                            \"another nice string\",r_ba,r_ba,r_aa,r_ba,r_ba,r_ba]";
795 
796     let result = simple_minify(source)
797         .apply(aggregate_strings)
798         .apply(::js::clean_tokens)
799         .to_string();
800     assert_eq!(result, expected_result);
801 }
802 
803 #[test]
already_existing_var()804 fn already_existing_var() {
805     let source = r#"var r_aa = "a nice string"; var x = ["a nice string", "a nice string",
806                     "another nice string", "cake!",
807                     "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
808     let expected_result = "var r_ba=\"cake!\";var r_aa=\"a nice string\";var x=[r_aa,r_aa,\
809                            \"another nice string\",r_ba,r_ba,r_aa,r_ba,r_ba,r_ba]";
810 
811     let result = simple_minify(source)
812         .apply(aggregate_strings)
813         .apply(::js::clean_tokens)
814         .to_string();
815     assert_eq!(result, expected_result);
816 }
817 
818 #[test]
string_duplicates_variables_already_exist()819 fn string_duplicates_variables_already_exist() {
820     let source = r#"var r_aa=1;var x = ["a nice string", "a nice string", "another nice string", "cake!",
821                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
822     let expected_result = "var r_ba=\"a nice string\",r_ca=\"cake!\";\
823                            var r_aa=1;var x=[r_ba,r_ba,\
824                            \"another nice string\",r_ca,r_ca,r_ba,r_ca,r_ca,r_ca]";
825 
826     let result = simple_minify(source)
827         .apply(aggregate_strings)
828         .apply(::js::clean_tokens)
829         .to_string();
830     assert_eq!(result, expected_result);
831 }
832 
833 #[test]
string_duplicates_with_separator()834 fn string_duplicates_with_separator() {
835     use self::token::ReservedChar;
836 
837     let source = r#"var x = ["a nice string", "a nice string", "another nice string", "cake!",
838                              "cake!", "a nice string", "cake!", "cake!", "cake!"];"#;
839     let expected_result = "var r_aa=\"a nice string\",r_ba=\"cake!\";\nvar x=[r_aa,r_aa,\
840                            \"another nice string\",r_ba,r_ba,r_aa,r_ba,r_ba,r_ba]";
841     let result = simple_minify(source)
842         .apply(::js::clean_tokens)
843         .apply(|f| aggregate_strings_with_separation(f, Token::Char(ReservedChar::Backline)))
844         .to_string();
845     assert_eq!(result, expected_result);
846 }
847 
848 #[test]
clean_except()849 fn clean_except() {
850     use self::token::ReservedChar;
851 
852     let source = r#"var x = [1, 2, 3];
853 var y = "salut";
854 var z = "ok!";"#;
855     let expected = r#"var x=[1,2,3];
856 var y="salut";
857 var z="ok!""#;
858 
859     let result = simple_minify(source)
860         .apply(|f| ::js::clean_tokens_except(f, |c| c.get_char() != Some(ReservedChar::Backline)))
861         .to_string();
862     assert_eq!(result, expected);
863 }
864 
865 #[test]
clean_except2()866 fn clean_except2() {
867     use self::token::ReservedChar;
868 
869     let source = "let x = [ 1, 2, \t3];";
870     let expected = "let x = [ 1, 2, 3];";
871 
872     let result = simple_minify(source)
873         .apply(|f| {
874             ::js::clean_tokens_except(f, |c| {
875                 c.get_char() != Some(ReservedChar::Space)
876                     && c.get_char() != Some(ReservedChar::SemiColon)
877             })
878         })
879         .to_string();
880     assert_eq!(result, expected);
881 }
882 
883 #[test]
clean_except3()884 fn clean_except3() {
885     use self::token::ReservedChar;
886 
887     let source = "let x = [ 1, 2, \t3];";
888     let expected = "let x=[1,2,\t3];";
889 
890     let result = simple_minify(source)
891         .apply(|f| {
892             ::js::clean_tokens_except(f, |c| {
893                 c.get_char() != Some(ReservedChar::Tab)
894                     && c.get_char() != Some(ReservedChar::SemiColon)
895             })
896         })
897         .to_string();
898     assert_eq!(result, expected);
899 }
900 
901 #[test]
name_generator()902 fn name_generator() {
903     let s = ::std::iter::repeat('a').take(36).collect::<String>();
904     // We need to generate enough long strings to reach the point that the name generator
905     // generates names with 3 characters.
906     let s = ::std::iter::repeat(s)
907         .take(20000)
908         .enumerate()
909         .map(|(pos, s)| format!("{}{}", s, pos))
910         .collect::<Vec<_>>();
911     let source = format!(
912         "var x = [{}];",
913         s.iter()
914             .map(|s| format!("\"{0}\",\"{0}\"", s))
915             .collect::<Vec<_>>()
916             .join(",")
917     );
918     let result = simple_minify(&source)
919         .apply(::js::clean_tokens)
920         .apply(aggregate_strings)
921         .to_string();
922     assert!(result.find(",r_aaa=").is_some());
923     assert!(result.find(",r_ab=").unwrap() < result.find(",r_ba=").unwrap());
924 }
925 
926 #[test]
simple_quote()927 fn simple_quote() {
928     let source = r#"var x = "\\";"#;
929     let expected_result = r#"var x="\\""#;
930     assert_eq!(minify(source), expected_result);
931 }
932 
933 #[test]
js_minify_test()934 fn js_minify_test() {
935     let source = r##"
936 var foo = "something";
937 
938 var another_var = 2348323;
939 
940 // who doesn't like comments?
941 /* and even longer comments?
942 
943 like
944 on
945 a
946 lot
947 of
948 lines!
949 
950 Fun!
951 */
952 function far_away(x, y) {
953     var x2 = x + 4;
954     return x * x2 + y;
955 }
956 
957 // this call is useless
958 far_away(another_var, 12);
959 // this call is useless too
960 far_away(another_var, 12);
961 "##;
962 
963     let expected_result = "var foo=\"something\";var another_var=2348323;function far_away(x,y){\
964                            var x2=x+4;return x*x2+y}far_away(another_var,12);far_away(another_var,\
965                            12)";
966     assert_eq!(minify(source), expected_result);
967 }
968 
969 #[test]
another_js_test()970 fn another_js_test() {
971     let source = r#"
972 /*! let's keep this license
973  *
974  * because everyone likes licenses!
975  *
976  * right?
977  */
978 
979 function forEach(data, func) {
980     for (var i = 0; i < data.length; ++i) {
981         func(data[i]);
982     }
983 }
984 
985 forEach([0, 1, 2, 3, 4,
986          5, 6, 7, 8, 9], function (x) {
987             console.log(x);
988          });
989 // I think we're done?
990 console.log('done!');
991 "#;
992 
993     let expected_result = r#"/*! let's keep this license
994  *
995  * because everyone likes licenses!
996  *
997  * right?
998  */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!')"#;
999     assert_eq!(minify(source), expected_result);
1000 }
1001 
1002 #[test]
comment_issue()1003 fn comment_issue() {
1004     let source = r#"
1005 search_input.onchange = function(e) {
1006     // Do NOT e.preventDefault() here. It will prevent pasting.
1007     clearTimeout(searchTimeout);
1008     // zero-timeout necessary here because at the time of event handler execution the
1009     // pasted content is not in the input field yet. Shouldn’t make any difference for
1010     // change, though.
1011     setTimeout(search, 0);
1012 };
1013 "#;
1014     let expected_result = "search_input.onchange=function(e){clearTimeout(searchTimeout);\
1015                            setTimeout(search,0)}";
1016     assert_eq!(minify(source), expected_result);
1017 }
1018 
1019 #[test]
missing_whitespace()1020 fn missing_whitespace() {
1021     let source = r#"
1022 for (var entry in results) {
1023     if (results.hasOwnProperty(entry)) {
1024         ar.push(results[entry]);
1025     }
1026 }"#;
1027     let expected_result = "for(var entry in results){if(results.hasOwnProperty(entry)){\
1028                            ar.push(results[entry])}}";
1029     assert_eq!(minify(source), expected_result);
1030 }
1031 
1032 #[test]
weird_regex_issue()1033 fn weird_regex_issue() {
1034     let source = r#"
1035 val = val.replace(/\_/g, "");
1036 
1037 var valGenerics = extractGenerics(val);"#;
1038     let expected_result = "val=val.replace(/\\_/g,\"\");var valGenerics=extractGenerics(val)";
1039     assert_eq!(minify(source), expected_result);
1040 }
1041 
1042 #[test]
keep_space()1043 fn keep_space() {
1044     let source = "return 12;return x;";
1045 
1046     let expected_result = "return 12;return x";
1047     assert_eq!(minify(source), expected_result);
1048 
1049     assert_eq!("t in e", minify("t in e"));
1050     assert_eq!("t+1 in e", minify("t + 1 in e"));
1051     assert_eq!("t-1 in e", minify("t - 1 in e"));
1052     assert_eq!("'a'in e", minify("'a' in e"));
1053     assert_eq!("/a/g in e", minify("/a/g in e"));
1054     assert_eq!("/a/i in e", minify("/a/i in e"));
1055 
1056     assert_eq!("t instanceof e", minify("t instanceof e"));
1057     assert_eq!("t+1 instanceof e", minify("t + 1 instanceof e"));
1058     assert_eq!("t-1 instanceof e", minify("t - 1 instanceof e"));
1059     assert_eq!("'a'instanceof e", minify("'a' instanceof e"));
1060     assert_eq!("/a/g instanceof e", minify("/a/g instanceof e"));
1061     assert_eq!("/a/i instanceof e", minify("/a/i instanceof e"));
1062 }
1063 
1064 #[test]
test_remove_extra_whitespace_before_typeof()1065 fn test_remove_extra_whitespace_before_typeof() {
1066     let source = "var x = typeof 'foo';var y = typeof x;case typeof 'foo': 'bla'";
1067 
1068     let expected_result = "var x=typeof'foo';var y=typeof x;case typeof'foo':'bla'";
1069     assert_eq!(minify(source), expected_result);
1070 }
1071 
1072 #[test]
test_remove_extra_whitespace_before_in()1073 fn test_remove_extra_whitespace_before_in() {
1074     let source = r#"if ("key" in ev && typeof ev) { return true; }
1075 if (x in ev && typeof ev) { return true; }
1076 if (true in ev) { return true; }"#;
1077 
1078     let expected_result = r#"if("key"in ev&&typeof ev){return true}if(x in ev&&typeof ev){return true}if(true in ev){return true}"#;
1079     assert_eq!(minify(source), expected_result);
1080 }
1081 
1082 #[test]
test_remove_extra_whitespace_before_operator()1083 fn test_remove_extra_whitespace_before_operator() {
1084     let source = "( x ) / 2; x / y;x /= y";
1085 
1086     let expected_result = "(x)/2;x/y;x/=y";
1087     assert_eq!(minify(source), expected_result);
1088 }
1089 
1090 #[test]
check_regex_syntax()1091 fn check_regex_syntax() {
1092     let source = "console.log(/MSIE|Trident|Edge/.test(window.navigator.userAgent));";
1093     let expected = "console.log(/MSIE|Trident|Edge/.test(window.navigator.userAgent))";
1094     assert_eq!(minify(source), expected);
1095 }
1096 
1097 #[test]
minify_minified()1098 fn minify_minified() {
1099     let source = "function (i, n, a) { i[n].type.replace(/ *;(.|\\s)*/,\"\")===t&&a.push(i[n].MathJax.elementJax);return a}";
1100     let expected = "function(i,n,a){i[n].type.replace(/ *;(.|\\s)*/,\"\")===t&&a.push(i[n].MathJax.elementJax);return a}";
1101     assert_eq!(minify(source), expected);
1102 }
1103 
1104 #[test]
check_string()1105 fn check_string() {
1106     let source = r###"
1107         const a = 123;
1108         const b = "123";
1109         const c = `the number is ${a}  <-- note the spaces here`;
1110         const d = `      ${a}         ${b}      `;
1111     "###;
1112     let expected = "const a=123;const b=\"123\";const c=`the number is ${a}  <-- note the spaces \
1113     here`;const d=`      ${a}         ${b}      `";
1114     assert_eq!(minify(source), expected);
1115 }
1116 
1117 // TODO: requires AST to fix this issue!
1118 /*#[test]
1119 fn no_semi_colon() {
1120     let source = r#"
1121 console.log(1)
1122 console.log(2)
1123 var x = 12;
1124 "#;
1125     let expected_result = r#"console.log(1);console.log(2);var x=12;"#;
1126     assert_eq!(minify(source), expected_result);
1127 }*/
1128 
1129 // TODO: requires AST to fix this issue!
1130 /*#[test]
1131 fn correct_replace_for_backline() {
1132     let source = r#"
1133 function foo() {
1134     return
1135     12;
1136 }
1137 "#;
1138     let expected_result = r#"function foo(){return 12;}"#;
1139     assert_eq!(minify(source), expected_result);
1140 }*/
1141