1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use crate::js::token::{Keyword, Operation, ReservedChar, Token, Tokens};
4use std::vec::IntoIter;
5
6pub(crate) struct VariableNameGenerator<'a> {
7 letter: char,
8 lower: Option<Box<VariableNameGenerator<'a>>>,
9 prepend: Option<&'a str>,
10}
11
12impl<'a> VariableNameGenerator<'a> {
13 pub(crate) fn new(prepend: Option<&'a str>, nb_letter: usize) -> VariableNameGenerator<'a> {
14 if nb_letter > 1 {
15 VariableNameGenerator {
16 letter: 'a',
17 lower: Some(Box::new(VariableNameGenerator::new(None, nb_letter - 1))),
18 prepend,
19 }
20 } else {
21 VariableNameGenerator {
22 letter: 'a',
23 lower: None,
24 prepend,
25 }
26 }
27 }
28
29 pub(crate) fn next(&mut self) {
30 self.incr_letters();
31 }
32
33 #[allow(clippy::inherent_to_string)]
34 pub(crate) fn to_string(&self) -> String {
35 if let Some(ref lower) = self.lower {
36 format!(
37 "{}{}{}",
38 self.prepend.unwrap_or(""),
39 self.letter,
40 lower.to_string()
41 )
42 } else {
43 format!("{}{}", self.prepend.unwrap_or(""), self.letter)
44 }
45 }
46
47 #[allow(dead_code)]
48 pub(crate) fn len(&self) -> usize {
49 let first = match self.prepend {
50 Some(s) => s.len(),
51 None => 0,
52 } + 1;
53 first
54 + match self.lower {
55 Some(ref s) => s.len(),
56 None => 0,
57 }
58 }
59
60 pub(crate) fn incr_letters(&mut self) {
61 let max = [('z', 'A'), ('Z', '0'), ('9', 'a')];
62
63 for (m, next) in &max {
64 if self.letter == *m {
65 self.letter = *next;
66 if self.letter == 'a' {
67 if let Some(ref mut lower) = self.lower {
68 lower.incr_letters();
69 } else {
70 self.lower = Some(Box::new(VariableNameGenerator::new(None, 1)));
71 }
72 }
73 return;
74 }
75 }
76 self.letter = ((self.letter as u8) + 1) as char;
77 }
78}
79
80/// Replace given tokens with others.
81///
82/// # Example
83///
84/// ```rust
85/// extern crate minifier;
86/// use minifier::js::{Keyword, Token, replace_tokens_with, simple_minify};
87///
88/// fn main() {
89/// let js = r#"
90/// function replaceByNull(data, func) {
91/// for (var i = 0; i < data.length; ++i) {
92/// if func(data[i]) {
93/// data[i] = null;
94/// }
95/// }
96/// }
97/// }"#.into();
98/// let js_minified = simple_minify(js)
99/// .apply(|f| {
100/// replace_tokens_with(f, |t| {
101/// if *t == Token::Keyword(Keyword::Null) {
102/// Some(Token::Other("N"))
103/// } else {
104/// None
105/// }
106/// })
107/// });
108/// println!("{}", js_minified.to_string());
109/// }
110/// ```
111///
112/// The previous code will have all its `null` keywords replaced with `N`. In such cases,
113/// don't forget to include the definition of `N` in the returned minified javascript:
114///
115/// ```js
116/// var N = null;
117/// ```
118#[inline]
119pub fn replace_tokens_with<'a, 'b: 'a, F: Fn(&Token<'a>) -> Option<Token<'b>>>(
120 mut tokens: Tokens<'a>,
121 callback: F,
122) -> Tokens<'a> {
123 for token: &mut Token<'_> in tokens.0.iter_mut() {
124 if let Some(t: Token<'_>) = callback(token) {
125 *token = t;
126 }
127 }
128 tokens
129}
130
131/// Replace a given token with another.
132#[inline]
133pub fn replace_token_with<'a, 'b: 'a, F: Fn(&Token<'a>) -> Option<Token<'b>>>(
134 token: Token<'a>,
135 callback: &F,
136) -> Token<'a> {
137 if let Some(t: Token<'_>) = callback(&token) {
138 t
139 } else {
140 token
141 }
142}
143
144/// When looping over `Tokens`, if you encounter `Keyword::Var`, `Keyword::Let` or
145/// `Token::Other` using this function will allow you to get the variable name's
146/// position and the variable value's position (if any).
147///
148/// ## Note
149///
150/// It'll return the value only if there is an `Operation::Equal` found.
151///
152/// # Examples
153///
154/// ```
155/// extern crate minifier;
156/// use minifier::js::{Keyword, get_variable_name_and_value_positions, simple_minify};
157///
158/// fn main() {
159/// let source = r#"var x = 1;var z;var y = "2";"#;
160/// let mut result = Vec::new();
161///
162/// let tokens = simple_minify(source);
163///
164/// for pos in 0..tokens.len() {
165/// match tokens[pos].get_keyword() {
166/// Some(k) if k == Keyword::Let || k == Keyword::Var => {
167/// if let Some(x) = get_variable_name_and_value_positions(&tokens, pos) {
168/// result.push(x);
169/// }
170/// }
171/// _ => {}
172/// }
173/// }
174/// assert_eq!(result, vec![(2, Some(6)), (10, None), (14, Some(22))]);
175/// }
176/// ```
177pub fn get_variable_name_and_value_positions<'a>(
178 tokens: &'a Tokens<'a>,
179 pos: usize,
180) -> Option<(usize, Option<usize>)> {
181 if pos >= tokens.len() {
182 return None;
183 }
184 let mut tmp = pos;
185 match tokens[pos] {
186 Token::Keyword(Keyword::Let) | Token::Keyword(Keyword::Var) => {
187 tmp += 1;
188 }
189 Token::Other(_) if pos > 0 => {
190 let mut pos = pos - 1;
191 while pos > 0 {
192 if tokens[pos].is_comment() || tokens[pos].is_white_character() {
193 pos -= 1;
194 } else if tokens[pos] == Token::Char(ReservedChar::Comma)
195 || tokens[pos] == Token::Keyword(Keyword::Let)
196 || tokens[pos] == Token::Keyword(Keyword::Var)
197 {
198 break;
199 } else {
200 return None;
201 }
202 }
203 }
204 _ => return None,
205 }
206 while tmp < tokens.len() {
207 if tokens[tmp].is_other() {
208 let mut tmp2 = tmp + 1;
209 while tmp2 < tokens.len() {
210 if tokens[tmp2] == Token::Operation(Operation::Equal) {
211 tmp2 += 1;
212 while tmp2 < tokens.len() {
213 let token = &tokens[tmp2];
214 if token.is_string()
215 || token.is_other()
216 || token.is_regex()
217 || token.is_number()
218 || token.is_floating_number()
219 {
220 return Some((tmp, Some(tmp2)));
221 } else if !tokens[tmp2].is_comment() && !tokens[tmp2].is_white_character() {
222 break;
223 }
224 tmp2 += 1;
225 }
226 break;
227 } else if matches!(
228 tokens[tmp2].get_char(),
229 Some(ReservedChar::Comma) | Some(ReservedChar::SemiColon)
230 ) {
231 return Some((tmp, None));
232 } else if !(tokens[tmp2].is_comment()
233 || tokens[tmp2].is_white_character()
234 && tokens[tmp2].get_char() != Some(ReservedChar::Backline))
235 {
236 break;
237 }
238 tmp2 += 1;
239 }
240 } else {
241 // We don't care about syntax errors.
242 }
243 tmp += 1;
244 }
245 None
246}
247
248#[inline]
249fn get_next<'a>(it: &mut IntoIter<Token<'a>>) -> Option<Token<'a>> {
250 for t: Token<'_> in it {
251 if t.is_comment() || t.is_white_character() {
252 continue;
253 }
254 return Some(t);
255 }
256 None
257}
258
259/// Convenient function used to clean useless tokens in a token list.
260///
261/// # Example
262///
263/// ```rust,no_run
264/// extern crate minifier;
265///
266/// use minifier::js::{clean_tokens, simple_minify};
267/// use std::fs;
268///
269/// fn main() {
270/// let content = fs::read("some_file.js").expect("file not found");
271/// let source = String::from_utf8_lossy(&content);
272/// let s = simple_minify(&source); // First we get the tokens list.
273/// let s = s.apply(clean_tokens); // We now have a cleaned token list!
274/// println!("result: {:?}", s);
275/// }
276/// ```
277pub fn clean_tokens(tokens: Tokens<'_>) -> Tokens<'_> {
278 let mut v = Vec::with_capacity(tokens.len() / 3 * 2);
279 let mut it = tokens.0.into_iter();
280
281 loop {
282 let token = get_next(&mut it);
283 if token.is_none() {
284 break;
285 }
286 let token = token.unwrap();
287 if token.is_white_character() {
288 continue;
289 } else if token.get_char() == Some(ReservedChar::SemiColon) {
290 if v.is_empty() {
291 continue;
292 }
293 if let Some(next) = get_next(&mut it) {
294 if next != Token::Char(ReservedChar::CloseCurlyBrace) {
295 v.push(token);
296 }
297 v.push(next);
298 }
299 continue;
300 }
301 v.push(token);
302 }
303 v.into()
304}
305
306/// Returns true if the token is a "useful" one (so not a comment or a "useless"
307/// character).
308pub fn clean_token(token: &Token<'_>, next_token: &Option<&Token<'_>>) -> bool {
309 !token.is_comment() && {
310 if let Some(x: ReservedChar) = token.get_char() {
311 !x.is_white_character()
312 && (x != ReservedChar::SemiColon
313 || *next_token != Some(&Token::Char(ReservedChar::CloseCurlyBrace)))
314 } else {
315 true
316 }
317 }
318}
319
320#[inline]
321fn get_next_except<'a, F: Fn(&Token<'a>) -> bool>(
322 it: &mut IntoIter<Token<'a>>,
323 f: &F,
324) -> Option<Token<'a>> {
325 for t: Token<'_> in it {
326 if (t.is_comment() || t.is_white_character()) && f(&t) {
327 continue;
328 }
329 return Some(t);
330 }
331 None
332}
333
334/// Same as `clean_tokens` except that if a token is considered as not desired,
335/// the callback is called. If the callback returns `false` as well, it will
336/// be removed.
337///
338/// # Example
339///
340/// ```rust,no_run
341/// extern crate minifier;
342///
343/// use minifier::js::{clean_tokens_except, simple_minify, ReservedChar};
344/// use std::fs;
345///
346/// fn main() {
347/// let content = fs::read("some_file.js").expect("file not found");
348/// let source = String::from_utf8_lossy(&content);
349/// let s = simple_minify(&source); // First we get the tokens list.
350/// let s = s.apply(|f| {
351/// clean_tokens_except(f, |c| {
352/// c.get_char() != Some(ReservedChar::Backline)
353/// })
354/// }); // We now have a cleaned token list which kept backlines!
355/// println!("result: {:?}", s);
356/// }
357/// ```
358pub fn clean_tokens_except<'a, F: Fn(&Token<'a>) -> bool>(tokens: Tokens<'a>, f: F) -> Tokens<'a> {
359 let mut v = Vec::with_capacity(tokens.len() / 3 * 2);
360 let mut it = tokens.0.into_iter();
361
362 loop {
363 let token = get_next_except(&mut it, &f);
364 if token.is_none() {
365 break;
366 }
367 let token = token.unwrap();
368 if token.is_white_character() {
369 if f(&token) {
370 continue;
371 }
372 } else if token.get_char() == Some(ReservedChar::SemiColon) {
373 if v.is_empty() {
374 if !f(&token) {
375 v.push(token);
376 }
377 continue;
378 }
379 if let Some(next) = get_next_except(&mut it, &f) {
380 if next != Token::Char(ReservedChar::CloseCurlyBrace) || !f(&token) {
381 v.push(token);
382 }
383 v.push(next);
384 } else if !f(&token) {
385 v.push(token);
386 }
387 continue;
388 }
389 v.push(token);
390 }
391 v.into()
392}
393
394/// Returns true if the token is a "useful" one (so not a comment or a "useless"
395/// character).
396#[inline]
397pub fn clean_token_except<'a, F: Fn(&Token<'a>) -> bool>(
398 token: &Token<'a>,
399 next_token: &Option<&Token<'_>>,
400 f: &F,
401) -> bool {
402 if !clean_token(token, next_token) {
403 !f(token)
404 } else {
405 true
406 }
407}
408
409pub(crate) fn get_array<'a>(
410 tokens: &'a Tokens<'a>,
411 array_name: &str,
412) -> Option<(Vec<usize>, usize)> {
413 let mut ret = Vec::new();
414
415 let mut looking_for_var = false;
416 let mut looking_for_equal = false;
417 let mut looking_for_array_start = false;
418 let mut getting_values = false;
419
420 for pos in 0..tokens.len() {
421 if looking_for_var {
422 match tokens[pos] {
423 Token::Other(s) => {
424 looking_for_var = false;
425 if s == array_name {
426 looking_for_equal = true;
427 }
428 }
429 ref s => {
430 looking_for_var = s.is_comment() || s.is_white_character();
431 }
432 }
433 } else if looking_for_equal {
434 match tokens[pos] {
435 Token::Operation(Operation::Equal) => {
436 looking_for_equal = false;
437 looking_for_array_start = true;
438 }
439 ref s => {
440 looking_for_equal = s.is_comment() || s.is_white_character();
441 }
442 }
443 } else if looking_for_array_start {
444 match tokens[pos] {
445 Token::Char(ReservedChar::OpenBracket) => {
446 looking_for_array_start = false;
447 getting_values = true;
448 }
449 ref s => {
450 looking_for_array_start = s.is_comment() || s.is_white_character();
451 }
452 }
453 } else if getting_values {
454 match &tokens[pos] {
455 Token::Char(ReservedChar::CloseBracket) => {
456 return Some((ret, pos));
457 }
458 s if s.is_comment() || s.is_white_character() => {}
459 _ => {
460 ret.push(pos);
461 }
462 }
463 } else {
464 match tokens[pos] {
465 Token::Keyword(Keyword::Let) | Token::Keyword(Keyword::Var) => {
466 looking_for_var = true;
467 }
468 _ => {}
469 }
470 }
471 }
472 None
473}
474
475#[test]
476fn check_get_array() {
477 let source: &str = r#"var x = [ ]; var y = ['hello',
478 12]; var z = []; var w = 12;"#;
479
480 let tokens: Tokens<'_> = crate::js::token::tokenize(source);
481
482 let ar: Option<(Vec, usize)> = get_array(&tokens, array_name:"x");
483 assert!(ar.is_some());
484 assert_eq!(ar.unwrap().1, 9);
485
486 let ar: Option<(Vec, usize)> = get_array(&tokens, array_name:"y");
487 assert!(ar.is_some());
488 assert_eq!(ar.unwrap().1, 27);
489
490 let ar: Option<(Vec, usize)> = get_array(&tokens, array_name:"z");
491 assert!(ar.is_some());
492 assert_eq!(ar.unwrap().1, 37);
493
494 let ar: Option<(Vec, usize)> = get_array(&tokens, array_name:"w");
495 assert!(ar.is_none());
496
497 let ar: Option<(Vec, usize)> = get_array(&tokens, array_name:"W");
498 assert!(ar.is_none());
499}
500
501#[test]
502fn check_get_variable_name_and_value_positions() {
503 let source = r#"var x = 1;var y = "2",we=4;"#;
504 let mut result = Vec::new();
505 let mut pos = 0;
506
507 let tokens = crate::js::token::tokenize(source);
508
509 while pos < tokens.len() {
510 if let Some(x) = get_variable_name_and_value_positions(&tokens, pos) {
511 result.push(x);
512 pos = x.0;
513 }
514 pos += 1;
515 }
516 assert_eq!(result, vec![(2, Some(6)), (10, Some(18)), (20, Some(22))]);
517
518 let mut result = Vec::new();
519 let tokens = crate::js::clean_tokens(tokens);
520 pos = 0;
521
522 while pos < tokens.len() {
523 if let Some(x) = get_variable_name_and_value_positions(&tokens, pos) {
524 result.push(x);
525 pos = x.0;
526 }
527 pos += 1;
528 }
529 assert_eq!(result, vec![(1, Some(3)), (6, Some(8)), (10, Some(12))]);
530}
531
532#[test]
533fn replace_tokens() {
534 let source: &str = r#"
535var x = ['a', 'b', null, 'd', {'x': null, 'e': null, 'z': 'w'}];
536var n = null;
537"#;
538 let expected_result: &str = "var x=['a','b',N,'d',{'x':N,'e':N,'z':'w'}];var n=N";
539
540 let res: Tokens<'_> = crate::js::simple_minify(source)
541 .apply(crate::js::clean_tokens)
542 .apply(|f: Tokens<'_>| {
543 replace_tokens_with(tokens:f, |t: &Token<'_>| {
544 if *t == Token::Keyword(Keyword::Null) {
545 Some(Token::Other("N"))
546 } else {
547 None
548 }
549 })
550 });
551 assert_eq!(res.to_string(), expected_result);
552}
553
554#[test]
555fn check_iterator() {
556 let source: &str = r#"
557var x = ['a', 'b', null, 'd', {'x': null, 'e': null, 'z': 'w'}];
558var n = null;
559"#;
560 let expected_result: &str = "var x=['a','b',N,'d',{'x':N,'e':N,'z':'w'}];var n=N;";
561
562 let mut iter: impl Iterator> = crate::js::simple_minify(source).into_iter().peekable();
563 let mut tokens: Vec> = Vec::new();
564 while let Some(token: Token<'_>) = iter.next() {
565 if crate::js::clean_token(&token, &iter.peek()) {
566 tokens.push(if token == Token::Keyword(Keyword::Null) {
567 Token::Other("N")
568 } else {
569 token
570 });
571 }
572 }
573 let tokens: Tokens = tokens.into();
574 assert_eq!(tokens.to_string(), expected_result);
575}
576