blob: f19d26ebbb8fe20977ac5e180a4e2fc828bf501b [file] [log] [blame]
// 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<Box<VariableNameGenerator<'a>>>,
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<Token<'b>>>(
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<'b>>>(
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<usize>)> {
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<Token<'a>>) -> Option<Token<'a>> {
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<Token<'a>>,
f: &F,
) -> Option<Token<'a>> {
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>, 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);
}