3 // Copyright (c) 2018 Guillaume Gomez
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:
12 // The above copyright notice and this permission notice shall be included in all
13 // copies or substantial portions of the Software.
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
23 use js
::token
::{self, Keyword, ReservedChar, Token, Tokens}
;
24 use js
::utils
::{get_array, get_variable_name_and_value_positions, VariableNameGenerator}
;
26 use std
::collections
::{HashMap, HashSet}
;
28 /*#[derive(Debug, Clone, PartialEq, Eq)]
30 Function(Function<'a>),
32 Variable(Variable<'a>),
33 Condition(token::Condition),
35 Operation(Operation<'a>),
39 fn is_condition(&self) -> bool {
41 Elem::Condition(_) => true,
47 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
55 #[derive(Clone, PartialEq, Eq, Debug)]
60 #[derive(Clone, PartialEq, Eq, Debug)]
65 #[derive(Clone, PartialEq, Eq, Debug)]
67 name: Option<&'a str>,
68 args: Vec<Argument<'a>>,
72 #[derive(Clone, PartialEq, Eq, Debug)]
75 value: Option<&'a str>,
78 /*struct Condition<'a> {
84 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
91 #[derive(Clone, PartialEq, Eq, Debug)]
94 condition: Vec<Elem<'a>>,
98 #[derive(Clone, PartialEq, Eq, Debug)]
99 struct Operation<'a> {
103 fn get_while_condition<'a>(tokens: &[token::Token<'a>], pos: &mut usize) -> Result<Vec<Elem<'a>>, String> {
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()),
113 let mut elems: Vec<Elem<'a>> = Vec::with_capacity(1);
115 while let Some(e) = tokens.get(*pos) {
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));
129 Err("Expected \")\", found nothing...".to_owned())
132 fn get_do<'a>(tokens: &[token::Token<'a>], pos: &mut usize) -> Result<Elem<'a>, String> {
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()),
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()),
147 let mut loop_ = Loop {
149 condition: condition,
152 Ok(Elem::Loop(loop_))
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) {
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 {
166 return Err("Unexpected \"}\"".to_owned());
170 if !start_with_paren {
173 Err("Expected \"}\" at the end of the block but didn't find one...".to_owned())
177 fn build_ast<'a>(v: &[token::Token<'a>]) -> Result<Elem<'a>, String> {
180 match get_block(v, &mut pos, false) {
181 Ok(ast) => Ok(Elem::Block(ast)),
186 /// Minifies a given JS source code.
191 /// extern crate minifier;
192 /// use minifier::js::minify;
196 /// function forEach(data, func) {
197 /// for (var i = 0; i < data.length; ++i) {
201 /// let js_minified = minify(js);
205 pub fn minify(source
: &str) -> String
{
206 token
::tokenize(source
)
207 .apply(::js
::clean_tokens
)
211 // TODO: No scope handling or anything. Might be nice as a second step to add it...
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();
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
)
225 if let Some(var_name
) = tokens
[var_pos
].get_other() {
226 if !var_name
.starts_with("r_") {
230 ret
.insert(var_name
);
232 if let Some(s
) = tokens
[value_pos
].get_string() {
233 variables
.insert(s
, (var_pos
, value_pos
));
243 fn aggregate_strings_inner
<'a
, 'b
: 'a
>(
244 mut tokens
: Tokens
<'a
>,
245 separation_token
: Option
<Token
<'b
>>,
247 let mut new_vars
= Vec
::with_capacity(50);
248 let mut to_replace
: Vec
<(usize, usize)> = Vec
::new();
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);
254 let mut var_gen
= VariableNameGenerator
::new(Some("r_"), 2);
255 let mut next_name
= var_gen
.to_string();
257 let (all_variables
, values
) = get_variables_name(&tokens
);
258 while all_variables
.contains(&next_name
.as_str()) {
260 next_name
= var_gen
.to_string();
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
));
272 let x
= strs
.entry(token
).or_insert_with(|| Vec
::with_capacity(1));
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()
282 validated
.insert(token
, next_name
.clone());
284 next_name
= var_gen
.to_string();
285 while all_variables
.contains(&next_name
.as_str()) {
287 next_name
= var_gen
.to_string();
293 let mut ret
= Vec
::with_capacity(validated
.len());
295 // We need this macro to avoid having to sort the set when not testing the crate.
297 macro_rules
! inner_loop
{
299 let mut $x
= $x
.into_iter().collect
::<Vec
<_
>>();
300 $x
.sort_unstable_by(|a
, b
| a
.1.cmp(&b
.1));
305 macro_rules! inner_loop {
311 for (token
, var_name
) in inner_loop
!(validated
) {
312 ret
.push((var_name
, strs
.remove(&token
).unwrap()));
317 if new_vars
.is_empty() {
318 new_vars
.push(Token
::Keyword(Keyword
::Var
));
320 new_vars
.push(Token
::Char(ReservedChar
::Comma
));
322 new_vars
.push(Token
::CreatedVarDecl(format
!(
324 var_name
, tokens
[positions
[0]]
326 for pos
in positions
{
327 tokens
.0[pos
] = Token
::CreatedVar(var_name
.clone());
330 if !new_vars
.is_empty() {
331 new_vars
.push(Token
::Char(ReservedChar
::SemiColon
));
333 for (to_replace_pos
, variable_pos
) in to_replace
{
334 tokens
.0[to_replace_pos
] = tokens
.0[variable_pos
].clone();
336 if let Some(token
) = separation_token
{
337 new_vars
.push(token
);
339 new_vars
.append(&mut tokens
.0);
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
351 /// extern crate minifier;
352 /// use minifier::js::{aggregate_strings, clean_tokens, simple_minify};
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);
366 pub fn aggregate_strings(tokens
: Tokens
<'_
>) -> Tokens
<'_
> {
367 aggregate_strings_inner(tokens
, None
)
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.
376 /// Let's add a backline between the created variables and the rest of the code:
379 /// extern crate minifier;
380 /// use minifier::js::{
381 /// aggregate_strings_with_separation,
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);
402 pub fn aggregate_strings_with_separation
<'a
, 'b
: 'a
>(
404 separation_token
: Token
<'b
>,
406 aggregate_strings_inner(tokens
, Some(separation_token
))
410 fn aggregate_strings_into_array_inner
<'a
, 'b
: 'a
, T
: Fn(&Tokens
<'a
>, usize) -> bool
>(
411 mut tokens
: Tokens
<'a
>,
413 separation_token
: Option
<Token
<'b
>>,
416 let mut to_insert
= Vec
::with_capacity(100);
417 let mut to_replace
= Vec
::with_capacity(100);
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),
429 let mut validated
: HashSet
<&str> = HashSet
::new();
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));
437 validated
.insert(&st
[1..st
.len() - 1]);
438 to_ignore
.insert(*s
);
442 array_pos_str
= array_pos
.to_string();
443 for pos
in 0..tokens
.len() {
444 if to_ignore
.contains(&pos
) {
447 let token
= &tokens
[pos
];
448 if let Some(str_token
) = token
.get_string() {
449 if !filter(&tokens
, pos
) {
452 let s
= &str_token
[1..str_token
.len() - 1];
455 .or_insert_with(|| (0, Vec
::with_capacity(1), true));
457 if x
.1.len() > 1 && !validated
.contains(s
) {
460 > (array_name
.len() + array_pos_str
.len() + 2) * x
.1.len()
461 + array_pos_str
.len()
464 validated
.insert(&str_token
[1..str_token
.len() - 1]);
467 array_pos_str
= array_pos
.to_string();
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.
483 // Compute the score based on:
484 // current number of digits * str length * str occurence
486 // ^ This second solution should bring even better results.
488 // ALSO: if an array with such strings already exists, it'd be worth it to recompute
490 let mut validated
= validated
.iter().map(|v
| (strs
[v
].0, v
)).collect
::<Vec
<_
>>();
491 validated
.sort_unstable_by(|(p1
, _
), (p2
, _
)| p2
.cmp(p1
));
493 if need_recreate
&& !validated
.is_empty() {
494 if let Some(token
) = separation_token
{
495 to_insert
.push((0, token
));
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
))));
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()));
512 if !create_array_entry
{
515 to_insert
.push((end_bracket
, Token
::CreatedVar(format
!("\"{}\"", *s
))));
516 if iter
.peek().is_none() && current_array_values
.is_empty() {
519 to_insert
.push((end_bracket
, Token
::Char(ReservedChar
::Comma
)));
522 for (pos
, rep
) in to_replace
.into_iter() {
525 for (pos
, rep
) in to_insert
.into_iter() {
526 tokens
.0.insert
(pos
, rep
);
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.
537 /// Let's add a backline between the created variables and the rest of the code:
540 /// extern crate minifier;
541 /// use minifier::js::{
542 /// aggregate_strings_into_array_with_separation,
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);
563 pub fn aggregate_strings_into_array_with_separation
<'a
, 'b
: 'a
>(
566 separation_token
: Token
<'b
>,
568 aggregate_strings_into_array_inner(tokens
, array_name
, Some(separation_token
), |_
, _
| true)
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
575 pub fn aggregate_strings_into_array_with_separation_filter
<'a
, 'b
: 'a
, T
>(
578 separation_token
: Token
<'b
>,
582 T
: Fn(&Tokens
<'a
>, usize) -> bool
,
584 aggregate_strings_into_array_inner(tokens
, array_name
, Some(separation_token
), filter
)
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
595 /// extern crate minifier;
596 /// use minifier::js::{aggregate_strings_into_array, clean_tokens, simple_minify};
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);
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)
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.
617 pub fn aggregate_strings_into_array_filter
<'a
, T
>(
623 T
: Fn(&Tokens
<'a
>, usize) -> bool
,
625 aggregate_strings_into_array_inner(tokens
, array_name
, None
, filter
)
628 /// Simple function to get the untouched token list. Useful in case you want to perform some
629 /// actions directly on it.
634 /// extern crate minifier;
635 /// use minifier::js::simple_minify;
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.
646 pub fn simple_minify(source
: &str) -> Tokens
<'_
> {
647 token
::tokenize(source
)
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]]";
657 let result
= simple_minify(source
)
658 .apply(::js
::clean_tokens
)
659 .apply(|c
| aggregate_strings_into_array(c
, "R"))
661 assert_eq
!(result
, expected_result
);
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]]";
668 let result
= simple_minify(source
)
669 .apply(::js
::clean_tokens
)
671 aggregate_strings_into_array_with_separation(
674 Token
::Char(ReservedChar
::Backline
),
678 assert_eq
!(result
, expected_result
);
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],\
685 let result
= simple_minify(source
)
686 .apply(::js
::clean_tokens
)
688 aggregate_strings_into_array_with_separation(
691 Token
::Char(ReservedChar
::Backline
),
695 assert_eq
!(result
, expected_result
);
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]}";
703 let result
= simple_minify(source
)
704 .apply(::js
::clean_tokens
)
706 aggregate_strings_into_array_with_separation_filter(
709 Token
::Char(ReservedChar
::Backline
),
712 || !tokens
[pos
- 1].eq_char(ReservedChar
::OpenBracket
)
713 || tokens
[pos
- 2].get_other() != Some("searchIndex")
718 assert_eq
!(result
, expected_result
);
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]}";
723 let result
= simple_minify(source
)
724 .apply(::js
::clean_tokens
)
726 aggregate_strings_into_array_with_separation_filter(
729 Token
::Char(ReservedChar
::Backline
),
732 || !tokens
[pos
- 1].eq_char(ReservedChar
::OpenBracket
)
733 || tokens
[pos
- 2].get_other() != Some("searchIndex")
738 assert_eq
!(result
, expected_result
);
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]]";
748 let result
= simple_minify(source
)
749 .apply(::js
::clean_tokens
)
750 .apply(|c
| aggregate_strings_into_array(c
, "R"))
752 assert_eq
!(result
, expected_result
);
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]]";
759 let result
= simple_minify(source
)
760 .apply(::js
::clean_tokens
)
761 .apply(|c
| aggregate_strings_into_array(c
, "R"))
763 assert_eq
!(result
, expected_result
);
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]]";
770 let result
= simple_minify(source
)
771 .apply(::js
::clean_tokens
)
772 .apply(|c
| aggregate_strings_into_array(c
, "R"))
774 assert_eq
!(result
, expected_result
);
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]]";
782 let result
= simple_minify(source
)
783 .apply(::js
::clean_tokens
)
784 .apply(|c
| aggregate_strings_into_array(c
, "R"))
786 assert_eq
!(result
, expected_result
);
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]";
796 let result
= simple_minify(source
)
797 .apply(aggregate_strings
)
798 .apply(::js
::clean_tokens
)
800 assert_eq
!(result
, expected_result
);
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]";
811 let result
= simple_minify(source
)
812 .apply(aggregate_strings
)
813 .apply(::js
::clean_tokens
)
815 assert_eq
!(result
, expected_result
);
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]";
826 let result
= simple_minify(source
)
827 .apply(aggregate_strings
)
828 .apply(::js
::clean_tokens
)
830 assert_eq
!(result
, expected_result
);
834 fn string_duplicates_with_separator() {
835 use self::token
::ReservedChar
;
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
)))
845 assert_eq
!(result
, expected_result
);
850 use self::token
::ReservedChar
;
852 let source
= r
#"var x = [1, 2, 3];
855 let expected
= r
#"var x=[1,2,3];
859 let result
= simple_minify(source
)
860 .apply(|f
| ::js
::clean_tokens_except(f
, |c
| c
.get_char() != Some(ReservedChar
::Backline
)))
862 assert_eq
!(result
, expected
);
867 use self::token
::ReservedChar
;
869 let source
= "let x = [ 1, 2, \t3];";
870 let expected
= "let x = [ 1, 2, 3];";
872 let result
= simple_minify(source
)
874 ::js
::clean_tokens_except(f
, |c
| {
875 c
.get_char() != Some(ReservedChar
::Space
)
876 && c
.get_char() != Some(ReservedChar
::SemiColon
)
880 assert_eq
!(result
, expected
);
885 use self::token
::ReservedChar
;
887 let source
= "let x = [ 1, 2, \t3];";
888 let expected
= "let x=[1,2,\t3];";
890 let result
= simple_minify(source
)
892 ::js
::clean_tokens_except(f
, |c
| {
893 c
.get_char() != Some(ReservedChar
::Tab
)
894 && c
.get_char() != Some(ReservedChar
::SemiColon
)
898 assert_eq
!(result
, expected
);
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
)
909 .map(|(pos
, s
)| format
!("{}{}", s
, pos
))
910 .collect
::<Vec
<_
>>();
911 let source
= format
!(
914 .map(|s
| format
!("\"{0}\",\"{0}\"", s
))
918 let result
= simple_minify(&source
)
919 .apply(::js
::clean_tokens
)
920 .apply(aggregate_strings
)
922 assert
!(result
.find(",r_aaa=").is_some());
923 assert
!(result
.find(",r_ab=").unwrap() < result
.find(",r_ba=").unwrap());
928 let source
= r
#"var x = "\\";"#;
929 let expected_result
= r
#"var x="\\""#;
930 assert_eq
!(minify(source
), expected_result
);
934 fn js_minify_test() {
936 var foo = "something";
938 var another_var = 2348323;
940 // who doesn't like comments?
941 /* and even longer comments?
952 function far_away(x, y) {
957 // this call is useless
958 far_away(another_var, 12);
959 // this call is useless too
960 far_away(another_var, 12);
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,\
966 assert_eq
!(minify(source
), expected_result
);
970 fn another_js_test() {
972 /*! let's keep this license
974 * because everyone likes licenses!
979 function forEach(data, func) {
980 for (var i = 0; i < data.length; ++i) {
985 forEach([0, 1, 2, 3, 4,
986 5, 6, 7, 8, 9], function (x) {
989 // I think we're done?
990 console.log('done!');
993 let expected_result
= r
#"/*! let's keep this license
995 * because everyone likes licenses!
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
);
1003 fn comment_issue() {
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
1011 setTimeout(search, 0);
1014 let expected_result
= "search_input.onchange=function(e){clearTimeout(searchTimeout);\
1015 setTimeout(search,0)}";
1016 assert_eq
!(minify(source
), expected_result
);
1020 fn missing_whitespace() {
1022 for (var entry in results) {
1023 if (results.hasOwnProperty(entry)) {
1024 ar.push(results[entry]);
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
);
1033 fn weird_regex_issue() {
1035 val = val.replace(/\_/g, "");
1037 var valGenerics = extractGenerics(val);"#;
1038 let expected_result
= "val=val.replace(/\\_/g,\"\");var valGenerics=extractGenerics(val)";
1039 assert_eq
!(minify(source
), expected_result
);
1044 let source
= "return 12;return x;";
1046 let expected_result
= "return 12;return x";
1047 assert_eq
!(minify(source
), expected_result
);
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"));
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"));
1065 fn test_remove_extra_whitespace_before_typeof() {
1066 let source
= "var x = typeof 'foo';var y = typeof x;case typeof 'foo': 'bla'";
1068 let expected_result
= "var x=typeof'foo';var y=typeof x;case typeof'foo':'bla'";
1069 assert_eq
!(minify(source
), expected_result
);
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; }"#;
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
);
1083 fn test_remove_extra_whitespace_before_operator() {
1084 let source
= "( x ) / 2; x / y;x /= y";
1086 let expected_result
= "(x)/2;x/y;x/=y";
1087 assert_eq
!(minify(source
), expected_result
);
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
);
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
);
1109 const c = `the number is ${a} <-- note the spaces here`;
1110 const d = ` ${a} ${b} `;
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
);
1117 // TODO: requires AST to fix this issue!
1119 fn no_semi_colon() {
1125 let expected_result = r#"console.log(1);console.log(2);var x=12;"#;
1126 assert_eq!(minify(source), expected_result);
1129 // TODO: requires AST to fix this issue!
1131 fn correct_replace_for_backline() {
1138 let expected_result = r#"function foo(){return 12;}"#;
1139 assert_eq!(minify(source), expected_result);