// Take a look at the license at the top of the repository in the LICENSE file. use crate::js::token::{Keyword, Operation, ReservedChar, Token, Tokens}; use std::vec::IntoIter; pub(crate) struct VariableNameGenerator<'a> { letter: char, lower: Option>>, prepend: Option<&'a str>, } impl<'a> VariableNameGenerator<'a> { pub(crate) fn new(prepend: Option<&'a str>, nb_letter: usize) -> VariableNameGenerator<'a> { if nb_letter > 1 { VariableNameGenerator { letter: 'a', lower: Some(Box::new(VariableNameGenerator::new(None, nb_letter - 1))), prepend, } } else { VariableNameGenerator { letter: 'a', lower: None, prepend, } } } pub(crate) fn next(&mut self) { self.incr_letters(); } #[allow(clippy::inherent_to_string)] pub(crate) fn to_string(&self) -> String { if let Some(ref lower) = self.lower { format!( "{}{}{}", self.prepend.unwrap_or(""), self.letter, lower.to_string() ) } else { format!("{}{}", self.prepend.unwrap_or(""), self.letter) } } #[allow(dead_code)] pub(crate) fn len(&self) -> usize { let first = match self.prepend { Some(s) => s.len(), None => 0, } + 1; first + match self.lower { Some(ref s) => s.len(), None => 0, } } pub(crate) fn incr_letters(&mut self) { let max = [('z', 'A'), ('Z', '0'), ('9', 'a')]; for (m, next) in &max { if self.letter == *m { self.letter = *next; if self.letter == 'a' { if let Some(ref mut lower) = self.lower { lower.incr_letters(); } else { self.lower = Some(Box::new(VariableNameGenerator::new(None, 1))); } } return; } } self.letter = ((self.letter as u8) + 1) as char; } } /// Replace given tokens with others. /// /// # Example /// /// ```rust /// extern crate minifier; /// use minifier::js::{Keyword, Token, replace_tokens_with, simple_minify}; /// /// fn main() { /// let js = r#" /// function replaceByNull(data, func) { /// for (var i = 0; i < data.length; ++i) { /// if func(data[i]) { /// data[i] = null; /// } /// } /// } /// }"#.into(); /// let js_minified = simple_minify(js) /// .apply(|f| { /// replace_tokens_with(f, |t| { /// if *t == Token::Keyword(Keyword::Null) { /// Some(Token::Other("N")) /// } else { /// None /// } /// }) /// }); /// println!("{}", js_minified.to_string()); /// } /// ``` /// /// The previous code will have all its `null` keywords replaced with `N`. In such cases, /// don't forget to include the definition of `N` in the returned minified javascript: /// /// ```js /// var N = null; /// ``` #[inline] pub fn replace_tokens_with<'a, 'b: 'a, F: Fn(&Token<'a>) -> Option>>( mut tokens: Tokens<'a>, callback: F, ) -> Tokens<'a> { for token in tokens.0.iter_mut() { if let Some(t) = callback(token) { *token = t; } } tokens } /// Replace a given token with another. #[inline] pub fn replace_token_with<'a, 'b: 'a, F: Fn(&Token<'a>) -> Option>>( token: Token<'a>, callback: &F, ) -> Token<'a> { if let Some(t) = callback(&token) { t } else { token } } /// When looping over `Tokens`, if you encounter `Keyword::Var`, `Keyword::Let` or /// `Token::Other` using this function will allow you to get the variable name's /// position and the variable value's position (if any). /// /// ## Note /// /// It'll return the value only if there is an `Operation::Equal` found. /// /// # Examples /// /// ``` /// extern crate minifier; /// use minifier::js::{Keyword, get_variable_name_and_value_positions, simple_minify}; /// /// fn main() { /// let source = r#"var x = 1;var z;var y = "2";"#; /// let mut result = Vec::new(); /// /// let tokens = simple_minify(source); /// /// for pos in 0..tokens.len() { /// match tokens[pos].get_keyword() { /// Some(k) if k == Keyword::Let || k == Keyword::Var => { /// if let Some(x) = get_variable_name_and_value_positions(&tokens, pos) { /// result.push(x); /// } /// } /// _ => {} /// } /// } /// assert_eq!(result, vec![(2, Some(6)), (10, None), (14, Some(22))]); /// } /// ``` pub fn get_variable_name_and_value_positions<'a>( tokens: &'a Tokens<'a>, pos: usize, ) -> Option<(usize, Option)> { if pos >= tokens.len() { return None; } let mut tmp = pos; match tokens[pos] { Token::Keyword(Keyword::Let) | Token::Keyword(Keyword::Var) => { tmp += 1; } Token::Other(_) if pos > 0 => { let mut pos = pos - 1; while pos > 0 { if tokens[pos].is_comment() || tokens[pos].is_white_character() { pos -= 1; } else if tokens[pos] == Token::Char(ReservedChar::Comma) || tokens[pos] == Token::Keyword(Keyword::Let) || tokens[pos] == Token::Keyword(Keyword::Var) { break; } else { return None; } } } _ => return None, } while tmp < tokens.len() { if tokens[tmp].is_other() { let mut tmp2 = tmp + 1; while tmp2 < tokens.len() { if tokens[tmp2] == Token::Operation(Operation::Equal) { tmp2 += 1; while tmp2 < tokens.len() { let token = &tokens[tmp2]; if token.is_string() || token.is_other() || token.is_regex() || token.is_number() || token.is_floating_number() { return Some((tmp, Some(tmp2))); } else if !tokens[tmp2].is_comment() && !tokens[tmp2].is_white_character() { break; } tmp2 += 1; } break; } else if matches!( tokens[tmp2].get_char(), Some(ReservedChar::Comma) | Some(ReservedChar::SemiColon) ) { return Some((tmp, None)); } else if !(tokens[tmp2].is_comment() || tokens[tmp2].is_white_character() && tokens[tmp2].get_char() != Some(ReservedChar::Backline)) { break; } tmp2 += 1; } } else { // We don't care about syntax errors. } tmp += 1; } None } #[inline] fn get_next<'a>(it: &mut IntoIter>) -> Option> { for t in it { if t.is_comment() || t.is_white_character() { continue; } return Some(t); } None } /// Convenient function used to clean useless tokens in a token list. /// /// # Example /// /// ```rust,no_run /// extern crate minifier; /// /// use minifier::js::{clean_tokens, simple_minify}; /// use std::fs; /// /// fn main() { /// let content = fs::read("some_file.js").expect("file not found"); /// let source = String::from_utf8_lossy(&content); /// let s = simple_minify(&source); // First we get the tokens list. /// let s = s.apply(clean_tokens); // We now have a cleaned token list! /// println!("result: {:?}", s); /// } /// ``` pub fn clean_tokens(tokens: Tokens<'_>) -> Tokens<'_> { let mut v = Vec::with_capacity(tokens.len() / 3 * 2); let mut it = tokens.0.into_iter(); loop { let token = get_next(&mut it); if token.is_none() { break; } let token = token.unwrap(); if token.is_white_character() { continue; } else if token.get_char() == Some(ReservedChar::SemiColon) { if v.is_empty() { continue; } if let Some(next) = get_next(&mut it) { if next != Token::Char(ReservedChar::CloseCurlyBrace) { v.push(token); } v.push(next); } continue; } v.push(token); } v.into() } /// Returns true if the token is a "useful" one (so not a comment or a "useless" /// character). pub fn clean_token(token: &Token<'_>, next_token: &Option<&Token<'_>>) -> bool { !token.is_comment() && { if let Some(x) = token.get_char() { !x.is_white_character() && (x != ReservedChar::SemiColon || *next_token != Some(&Token::Char(ReservedChar::CloseCurlyBrace))) } else { true } } } #[inline] fn get_next_except<'a, F: Fn(&Token<'a>) -> bool>( it: &mut IntoIter>, f: &F, ) -> Option> { for t in it { if (t.is_comment() || t.is_white_character()) && f(&t) { continue; } return Some(t); } None } /// Same as `clean_tokens` except that if a token is considered as not desired, /// the callback is called. If the callback returns `false` as well, it will /// be removed. /// /// # Example /// /// ```rust,no_run /// extern crate minifier; /// /// use minifier::js::{clean_tokens_except, simple_minify, ReservedChar}; /// use std::fs; /// /// fn main() { /// let content = fs::read("some_file.js").expect("file not found"); /// let source = String::from_utf8_lossy(&content); /// let s = simple_minify(&source); // First we get the tokens list. /// let s = s.apply(|f| { /// clean_tokens_except(f, |c| { /// c.get_char() != Some(ReservedChar::Backline) /// }) /// }); // We now have a cleaned token list which kept backlines! /// println!("result: {:?}", s); /// } /// ``` pub fn clean_tokens_except<'a, F: Fn(&Token<'a>) -> bool>(tokens: Tokens<'a>, f: F) -> Tokens<'a> { let mut v = Vec::with_capacity(tokens.len() / 3 * 2); let mut it = tokens.0.into_iter(); loop { let token = get_next_except(&mut it, &f); if token.is_none() { break; } let token = token.unwrap(); if token.is_white_character() { if f(&token) { continue; } } else if token.get_char() == Some(ReservedChar::SemiColon) { if v.is_empty() { if !f(&token) { v.push(token); } continue; } if let Some(next) = get_next_except(&mut it, &f) { if next != Token::Char(ReservedChar::CloseCurlyBrace) || !f(&token) { v.push(token); } v.push(next); } else if !f(&token) { v.push(token); } continue; } v.push(token); } v.into() } /// Returns true if the token is a "useful" one (so not a comment or a "useless" /// character). #[inline] pub fn clean_token_except<'a, F: Fn(&Token<'a>) -> bool>( token: &Token<'a>, next_token: &Option<&Token<'_>>, f: &F, ) -> bool { if !clean_token(token, next_token) { !f(token) } else { true } } pub(crate) fn get_array<'a>( tokens: &'a Tokens<'a>, array_name: &str, ) -> Option<(Vec, usize)> { let mut ret = Vec::new(); let mut looking_for_var = false; let mut looking_for_equal = false; let mut looking_for_array_start = false; let mut getting_values = false; for pos in 0..tokens.len() { if looking_for_var { match tokens[pos] { Token::Other(s) => { looking_for_var = false; if s == array_name { looking_for_equal = true; } } ref s => { looking_for_var = s.is_comment() || s.is_white_character(); } } } else if looking_for_equal { match tokens[pos] { Token::Operation(Operation::Equal) => { looking_for_equal = false; looking_for_array_start = true; } ref s => { looking_for_equal = s.is_comment() || s.is_white_character(); } } } else if looking_for_array_start { match tokens[pos] { Token::Char(ReservedChar::OpenBracket) => { looking_for_array_start = false; getting_values = true; } ref s => { looking_for_array_start = s.is_comment() || s.is_white_character(); } } } else if getting_values { match &tokens[pos] { Token::Char(ReservedChar::CloseBracket) => { return Some((ret, pos)); } s if s.is_comment() || s.is_white_character() => {} _ => { ret.push(pos); } } } else { match tokens[pos] { Token::Keyword(Keyword::Let) | Token::Keyword(Keyword::Var) => { looking_for_var = true; } _ => {} } } } None } #[test] fn check_get_array() { let source = r#"var x = [ ]; var y = ['hello', 12]; var z = []; var w = 12;"#; let tokens = crate::js::token::tokenize(source); let ar = get_array(&tokens, "x"); assert!(ar.is_some()); assert_eq!(ar.unwrap().1, 9); let ar = get_array(&tokens, "y"); assert!(ar.is_some()); assert_eq!(ar.unwrap().1, 27); let ar = get_array(&tokens, "z"); assert!(ar.is_some()); assert_eq!(ar.unwrap().1, 37); let ar = get_array(&tokens, "w"); assert!(ar.is_none()); let ar = get_array(&tokens, "W"); assert!(ar.is_none()); } #[test] fn check_get_variable_name_and_value_positions() { let source = r#"var x = 1;var y = "2",we=4;"#; let mut result = Vec::new(); let mut pos = 0; let tokens = crate::js::token::tokenize(source); while pos < tokens.len() { if let Some(x) = get_variable_name_and_value_positions(&tokens, pos) { result.push(x); pos = x.0; } pos += 1; } assert_eq!(result, vec![(2, Some(6)), (10, Some(18)), (20, Some(22))]); let mut result = Vec::new(); let tokens = crate::js::clean_tokens(tokens); pos = 0; while pos < tokens.len() { if let Some(x) = get_variable_name_and_value_positions(&tokens, pos) { result.push(x); pos = x.0; } pos += 1; } assert_eq!(result, vec![(1, Some(3)), (6, Some(8)), (10, Some(12))]); } #[test] fn replace_tokens() { let source = r#" var x = ['a', 'b', null, 'd', {'x': null, 'e': null, 'z': 'w'}]; var n = null; "#; let expected_result = "var x=['a','b',N,'d',{'x':N,'e':N,'z':'w'}];var n=N"; let res = crate::js::simple_minify(source) .apply(crate::js::clean_tokens) .apply(|f| { replace_tokens_with(f, |t| { if *t == Token::Keyword(Keyword::Null) { Some(Token::Other("N")) } else { None } }) }); assert_eq!(res.to_string(), expected_result); } #[test] fn check_iterator() { let source = r#" var x = ['a', 'b', null, 'd', {'x': null, 'e': null, 'z': 'w'}]; var n = null; "#; let expected_result = "var x=['a','b',N,'d',{'x':N,'e':N,'z':'w'}];var n=N;"; let mut iter = crate::js::simple_minify(source).into_iter().peekable(); let mut tokens = Vec::new(); while let Some(token) = iter.next() { if crate::js::clean_token(&token, &iter.peek()) { tokens.push(if token == Token::Keyword(Keyword::Null) { Token::Other("N") } else { token }); } } let tokens: Tokens = tokens.into(); assert_eq!(tokens.to_string(), expected_result); }