openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

JSqueeze.class.php (28942B)


      1 <?php
      2 
      3 /*
      4  * Copyright (C) 2016 Nicolas Grekas - p@tchwork.com
      5  *
      6  * This library is free software; you can redistribute it and/or modify it
      7  * under the terms of the (at your option):
      8  * Apache License v2.0 (see provided LICENCE.ASL20 file), or
      9  * GNU General Public License v2.0 (see provided LICENCE.GPLv2 file).
     10  */
     11 
     12 //namespace Patchwork;
     13 
     14 /*
     15 *
     16 * This class shrinks Javascript code
     17 * (a process called minification nowadays)
     18 *
     19 * Should work with most valid Javascript code,
     20 * even when semi-colons are missing.
     21 *
     22 * Features:
     23 * - Removes comments and white spaces.
     24 * - Renames every local vars, typically to a single character.
     25 * - Renames also global vars, methods and properties, but only if they
     26 *   are marked special by some naming convention. By default, special
     27 *   var names begin with one or more "$", or with a single "_".
     28 * - Renames also local/global vars found in strings,
     29 *   but only if they are marked special.
     30 * - Keep Microsoft's conditional comments.
     31 * - Output is optimized for later HTTP compression.
     32 *
     33 * Notes:
     34 * - Source code must be parse error free before processing.
     35 * - In order to maximise later HTTP compression (deflate, gzip),
     36 *   new variables names are chosen by considering closures,
     37 *   variables' frequency and characters' frequency.
     38 * - If you use with/eval then be careful.
     39 *
     40 * Bonus:
     41 * - Replaces false/true by !1/!0
     42 * - Replaces new Array/Object by []/{}
     43 * - Merges consecutive "var" declarations with commas
     44 * - Merges consecutive concatened strings
     45 * - Fix a bug in Safari's parser (http://forums.asp.net/thread/1585609.aspx)
     46 * - Can replace optional semi-colons by line feeds,
     47 *   thus facilitating output debugging.
     48 * - Keep important comments marked with /*!...
     49 * - Treats three semi-colons ;;; like single-line comments
     50 *   (http://dean.edwards.name/packer/2/usage/#triple-semi-colon).
     51 * - Fix special catch scope across browsers
     52 * - Work around buggy-handling of named function expressions in IE<=8
     53 *
     54 * TODO?
     55 * - foo['bar'] => foo.bar
     56 * - {'foo':'bar'} => {foo:'bar'}
     57 * - Dead code removal (never used function)
     58 * - Munge primitives: var WINDOW=window, etc.
     59 */
     60 
     61 namespace util;
     62 class JSqueeze
     63 {
     64 	const
     65 
     66 		SPECIAL_VAR_PACKER = '(\$+[a-zA-Z_]|_[a-zA-Z0-9$])[a-zA-Z0-9_$]*';
     67 
     68 	public
     69 
     70 		$charFreq;
     71 
     72 	protected
     73 
     74 		$strings,
     75 		$closures,
     76 		$str0,
     77 		$str1,
     78 		$argFreq,
     79 		$specialVarRx,
     80 		$keepImportantComments,
     81 
     82 		$varRx = '(?:[a-zA-Z_$])[a-zA-Z0-9_$]*',
     83 		$reserved = array(
     84 		// Literals
     85 		'true', 'false', 'null',
     86 		// ES6
     87 		'break', 'case', 'class', 'catch', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield',
     88 		// Future
     89 		'enum',
     90 		// Strict mode
     91 		'implements', 'package', 'protected', 'static', 'let', 'interface', 'private', 'public',
     92 		// Module
     93 		'await',
     94 		// Older standards
     95 		'abstract', 'boolean', 'byte', 'char', 'double', 'final', 'float', 'goto', 'int', 'long', 'native', 'short', 'synchronized', 'throws', 'transient', 'volatile',
     96 	);
     97 
     98 	public function __construct()
     99 	{
    100 		$this->reserved = array_flip($this->reserved);
    101 		$this->charFreq = array_fill(0, 256, 0);
    102 	}
    103 
    104 	/**
    105 	 * Squeezes a JavaScript source code.
    106 	 *
    107 	 * Set $singleLine to false if you want optional
    108 	 * semi-colons to be replaced by line feeds.
    109 	 *
    110 	 * Set $keepImportantComments to false if you want /*! comments to be removed.
    111 	 *
    112 	 * $specialVarRx defines the regular expression of special variables names
    113 	 * for global vars, methods, properties and in string substitution.
    114 	 * Set it to false if you don't want any.
    115 	 *
    116 	 * If the analysed javascript source contains a single line comment like
    117 	 * this one, then the directive will overwrite $specialVarRx:
    118 	 *
    119 	 * // jsqueeze.specialVarRx = your_special_var_regexp_here
    120 	 *
    121 	 * Only the first directive is parsed, others are ignored. It is not possible
    122 	 * to redefine $specialVarRx in the middle of the javascript source.
    123 	 *
    124 	 * Example:
    125 	 * $parser = new JSqueeze;
    126 	 * $squeezed_js = $parser->squeeze($fat_js);
    127 	 */
    128 	public function squeeze($code, $singleLine = true, $keepImportantComments = true, $specialVarRx = false)
    129 	{
    130 		$code = trim($code);
    131 		if ('' === $code) {
    132 			return '';
    133 		}
    134 
    135 		$this->argFreq = array(-1 => 0);
    136 		$this->specialVarRx = $specialVarRx;
    137 		$this->keepImportantComments = !!$keepImportantComments;
    138 
    139 		if (preg_match("#//[ \t]*jsqueeze\.specialVarRx[ \t]*=[ \t]*([\"']?)(.*)\\1#i", $code, $key)) {
    140 			if (!$key[1]) {
    141 				$key[2] = trim($key[2]);
    142 				$key[1] = strtolower($key[2]);
    143 				$key[1] = $key[1] && $key[1] != 'false' && $key[1] != 'none' && $key[1] != 'off';
    144 			}
    145 
    146 			$this->specialVarRx = $key[1] ? $key[2] : false;
    147 		}
    148 
    149 		// Remove capturing parentheses
    150 		$this->specialVarRx && $this->specialVarRx = preg_replace('/(?<!\\\\)((?:\\\\\\\\)*)\((?!\?)/', '(?:', $this->specialVarRx);
    151 
    152 		false !== strpos($code, "\r") && $code = strtr(str_replace("\r\n", "\n", $code), "\r", "\n");
    153 		false !== strpos($code, "\xC2\x85") && $code = str_replace("\xC2\x85", "\n", $code); // Next Line
    154 		false !== strpos($code, "\xE2\x80\xA8") && $code = str_replace("\xE2\x80\xA8", "\n", $code); // Line Separator
    155 		false !== strpos($code, "\xE2\x80\xA9") && $code = str_replace("\xE2\x80\xA9", "\n", $code); // Paragraph Separator
    156 
    157 		list($code, $this->strings) = $this->extractStrings($code);
    158 		list($code, $this->closures) = $this->extractClosures($code);
    159 
    160 		$key = "//''\"\"#0'"; // This crap has a wonderful property: it can not happen in any valid javascript, even in strings
    161 		$this->closures[$key] = &$code;
    162 
    163 		$tree = array($key => array('parent' => false));
    164 		$this->makeVars($code, $tree[$key], $key);
    165 		$this->renameVars($tree[$key], true);
    166 
    167 		$code = substr($tree[$key]['code'], 1);
    168 		$code = preg_replace("'\breturn !'", 'return!', $code);
    169 		$code = preg_replace("'\}(?=(else|while)[^\$.a-zA-Z0-9_])'", "}\r", $code);
    170 		$code = str_replace(array_keys($this->strings), array_values($this->strings), $code);
    171 
    172 		if ($singleLine) {
    173 			$code = strtr($code, "\n", ';');
    174 		} else {
    175 			$code = str_replace("\n", ";\n", $code);
    176 		}
    177 		false !== strpos($code, "\r") && $code = strtr(trim($code), "\r", "\n");
    178 
    179 		// Cleanup memory
    180 		$this->charFreq = array_fill(0, 256, 0);
    181 		$this->strings = $this->closures = $this->argFreq = array();
    182 		$this->str0 = $this->str1 = '';
    183 
    184 		return $code;
    185 	}
    186 
    187 	protected function extractStrings($f)
    188 	{
    189 		if ($cc_on = false !== strpos($f, '@cc_on')) {
    190 			// Protect conditional comments from being removed
    191 			$f = str_replace('#', '##', $f);
    192 			$f = str_replace('/*@', '1#@', $f);
    193 			$f = preg_replace("'//@([^\n]+)'", '2#@$1@#3', $f);
    194 			$f = str_replace('@*/', '@#1', $f);
    195 		}
    196 
    197 		$len = strlen($f);
    198 		$code = str_repeat(' ', $len);
    199 		$j = 0;
    200 
    201 		$strings = array();
    202 		$K = 0;
    203 
    204 		$instr = false;
    205 
    206 		$q = array(
    207 			"'", '"',
    208 			"'" => 0,
    209 			'"' => 0,
    210 		);
    211 
    212 		// Extract strings, removes comments
    213 		for ($i = 0; $i < $len; ++$i) {
    214 			if ($instr) {
    215 				if ('//' == $instr) {
    216 					if ("\n" == $f[$i]) {
    217 						$f[$i--] = ' ';
    218 						$instr = false;
    219 					}
    220 				} elseif ($f[$i] == $instr || ('/' == $f[$i] && "/'" == $instr)) {
    221 					if ('!' == $instr) {
    222 					} elseif ('*' == $instr) {
    223 						if ('/' == $f[$i + 1]) {
    224 							++$i;
    225 							$instr = false;
    226 						}
    227 					} else {
    228 						if ("/'" == $instr) {
    229 							while (isset($f[$i + 1]) && false !== strpos('gmi', $f[$i + 1])) {
    230 								$s[] = $f[$i++];
    231 							}
    232 							$s[] = $f[$i];
    233 						}
    234 
    235 						$instr = false;
    236 					}
    237 				} elseif ('*' == $instr) {
    238 				} elseif ('!' == $instr) {
    239 					if ('*' == $f[$i] && '/' == $f[$i + 1]) {
    240 						$s[] = "*/\r";
    241 						++$i;
    242 						$instr = false;
    243 					} elseif ("\n" == $f[$i]) {
    244 						$s[] = "\r";
    245 					} else {
    246 						$s[] = $f[$i];
    247 					}
    248 				} elseif ('\\' == $f[$i]) {
    249 					++$i;
    250 
    251 					if ("\n" != $f[$i]) {
    252 						isset($q[$f[$i]]) && ++$q[$f[$i]];
    253 						$s[] = '\\' . $f[$i];
    254 					}
    255 				} elseif ('[' == $f[$i] && "/'" == $instr) {
    256 					$instr = '/[';
    257 					$s[] = '[';
    258 				} elseif (']' == $f[$i] && '/[' == $instr) {
    259 					$instr = "/'";
    260 					$s[] = ']';
    261 				} elseif ("'" == $f[$i] || '"' == $f[$i]) {
    262 					++$q[$f[$i]];
    263 					$s[] = '\\' . $f[$i];
    264 				} else {
    265 					$s[] = $f[$i];
    266 				}
    267 			} else {
    268 				switch ($f[$i]) {
    269 					case ';':
    270 						// Remove triple semi-colon
    271 						if ($i > 0 && ';' == $f[$i - 1] && $i + 1 < $len && ';' == $f[$i + 1]) {
    272 							$f[$i] = $f[$i + 1] = '/';
    273 						} else {
    274 							$code[++$j] = ';';
    275 							break;
    276 						}
    277 
    278 					case '/':
    279 						if ('*' == $f[$i + 1]) {
    280 							++$i;
    281 							$instr = '*';
    282 
    283 							if ($this->keepImportantComments && '!' == $f[$i + 1]) {
    284 								++$i;
    285 								// no break here
    286 							} else {
    287 								break;
    288 							}
    289 						} elseif ('/' == $f[$i + 1]) {
    290 							++$i;
    291 							$instr = '//';
    292 							break;
    293 						} else {
    294 							$a = $j && (' ' == $code[$j] || "\x7F" == $code[$j]) ? $code[$j - 1] : $code[$j];
    295 							if (false !== strpos('-!%&;<=>~:^+|,()*?[{} ', $a)
    296 								|| (false !== strpos('oenfd', $a)
    297 									&& preg_match(
    298 										"'(?<![\$.a-zA-Z0-9_])(do|else|return|typeof|yield[ \x7F]?\*?)[ \x7F]?$'",
    299 										substr($code, $j - 7, 8)
    300 									))
    301 							) {
    302 								if (')' === $a && $j > 1) {
    303 									$a = 1;
    304 									$k = $j - (' ' == $code[$j] || "\x7F" == $code[$j]) - 1;
    305 									while ($k >= 0 && $a) {
    306 										if ('(' === $code[$k]) {
    307 											--$a;
    308 										} elseif (')' === $code[$k]) {
    309 											++$a;
    310 										}
    311 										--$k;
    312 									}
    313 									if (!preg_match("'(?<![\$.a-zA-Z0-9_])(if|for|while)[ \x7F]?$'", substr($code, 0, $k + 1))) {
    314 										$code[++$j] = '/';
    315 										break;
    316 									}
    317 								}
    318 
    319 								$key = "//''\"\"" . $K++ . $instr = "/'";
    320 								$a = $j;
    321 								$code .= $key;
    322 								while (isset($key[++$j - $a - 1])) {
    323 									$code[$j] = $key[$j - $a - 1];
    324 								}
    325 								--$j;
    326 								isset($s) && ($s = implode('', $s)) && $cc_on && $this->restoreCc($s);
    327 								$strings[$key] = array('/');
    328 								$s = &$strings[$key];
    329 							} else {
    330 								$code[++$j] = '/';
    331 							}
    332 
    333 							break;
    334 						}
    335 
    336 					case "'":
    337 					case '"':
    338 						$instr = $f[$i];
    339 						$key = "//''\"\"" . $K++ . ('!' == $instr ? ']' : "'");
    340 						$a = $j;
    341 						$code .= $key;
    342 						while (isset($key[++$j - $a - 1])) {
    343 							$code[$j] = $key[$j - $a - 1];
    344 						}
    345 						--$j;
    346 						isset($s) && ($s = implode('', $s)) && $cc_on && $this->restoreCc($s);
    347 						$strings[$key] = array();
    348 						$s = &$strings[$key];
    349 						'!' == $instr && $s[] = "\r/*!";
    350 
    351 						break;
    352 
    353 					case "\n":
    354 						if ($j > 3) {
    355 							if (' ' == $code[$j] || "\x7F" == $code[$j]) {
    356 								--$j;
    357 							}
    358 
    359 							$code[++$j] =
    360 								false !== strpos('kend+-', $code[$j - 1])
    361 								&& preg_match(
    362 									"'(?:\+\+|--|(?<![\$.a-zA-Z0-9_])(break|continue|return|yield[ \x7F]?\*?))[ \x7F]?$'",
    363 									substr($code, $j - 8, 9)
    364 								)
    365 									? ';' : "\x7F";
    366 
    367 							break;
    368 						}
    369 
    370 					case "\t":
    371 						$f[$i] = ' ';
    372 					case ' ':
    373 						if (!$j || ' ' == $code[$j] || "\x7F" == $code[$j]) {
    374 							break;
    375 						}
    376 
    377 					default:
    378 						$code[++$j] = $f[$i];
    379 				}
    380 			}
    381 		}
    382 
    383 		isset($s) && ($s = implode('', $s)) && $cc_on && $this->restoreCc($s);
    384 		unset($s);
    385 
    386 		$code = substr($code, 0, $j + 1);
    387 		$cc_on && $this->restoreCc($code, false);
    388 
    389 		// Protect wanted spaces and remove unwanted ones
    390 		$code = strtr($code, "\x7F", ' ');
    391 		$code = str_replace('- -', "-\x7F-", $code);
    392 		$code = str_replace('+ +', "+\x7F+", $code);
    393 		$code = preg_replace("'(\d)\s+\.\s*([a-zA-Z\$_[(])'", "$1\x7F.$2", $code);
    394 		$code = preg_replace("# ([-!%&;<=>~:.^+|,()*?[\]{}/']+)#", '$1', $code);
    395 		$code = preg_replace("#([-!%&;<=>~:.^+|,()*?[\]{}/]+) #", '$1', $code);
    396 		$cc_on && $code = preg_replace_callback("'//[^\'].*?@#3'", function ($m) {
    397 			return strtr($m[0], ' ', "\x7F");
    398 		}, $code);
    399 
    400 		// Replace new Array/Object by []/{}
    401 		false !== strpos($code, 'new Array') && $code = preg_replace("'new Array(?:\(\)|([;\])},:]))'", '[]$1', $code);
    402 		false !== strpos($code, 'new Object') && $code = preg_replace("'new Object(?:\(\)|([;\])},:]))'", '{}$1', $code);
    403 
    404 		// Add missing semi-colons after curly braces
    405 		// This adds more semi-colons than strictly needed,
    406 		// but it seems that later gzipping is favorable to the repetition of "};"
    407 		$code = preg_replace("'\}(?![:,;.()\[\]}\|&]|(else|catch|finally|while)[^\$.a-zA-Z0-9_])'", '};', $code);
    408 
    409 		// Tag possible empty instruction for easy detection
    410 		$code = preg_replace("'(?<![\$.a-zA-Z0-9_])if\('", '1#(', $code);
    411 		$code = preg_replace("'(?<![\$.a-zA-Z0-9_])for\('", '2#(', $code);
    412 		$code = preg_replace("'(?<![\$.a-zA-Z0-9_])while\('", '3#(', $code);
    413 
    414 		$forPool = array();
    415 		$instrPool = array();
    416 		$s = 0;
    417 
    418 		$f = array();
    419 		$j = -1;
    420 
    421 		// Remove as much semi-colon as possible
    422 		$len = strlen($code);
    423 		for ($i = 0; $i < $len; ++$i) {
    424 			switch ($code[$i]) {
    425 				case '(':
    426 					if ($j >= 0 && "\n" == $f[$j]) {
    427 						$f[$j] = ';';
    428 					}
    429 
    430 					++$s;
    431 
    432 					if ($i && '#' == $code[$i - 1]) {
    433 						$instrPool[$s - 1] = 1;
    434 						if ('2' == $code[$i - 2]) {
    435 							$forPool[$s] = 1;
    436 						}
    437 					}
    438 
    439 					$f[++$j] = '(';
    440 					break;
    441 
    442 				case ']':
    443 				case ')':
    444 					if ($i + 1 < $len && !isset($forPool[$s]) && !isset($instrPool[$s - 1]) && preg_match("'[a-zA-Z0-9_\$]'", $code[$i + 1])) {
    445 						$f[$j] .= $code[$i];
    446 						$f[++$j] = "\n";
    447 					} else {
    448 						$f[++$j] = $code[$i];
    449 					}
    450 
    451 					if (')' == $code[$i]) {
    452 						unset($forPool[$s]);
    453 						--$s;
    454 					}
    455 
    456 					continue 2;
    457 
    458 				case '}':
    459 					if ("\n" == $f[$j]) {
    460 						$f[$j] = '}';
    461 					} else {
    462 						$f[++$j] = '}';
    463 					}
    464 					break;
    465 
    466 				case ';':
    467 					if (isset($forPool[$s]) || isset($instrPool[$s])) {
    468 						$f[++$j] = ';';
    469 					} elseif ($j >= 0 && "\n" != $f[$j] && ';' != $f[$j]) {
    470 						$f[++$j] = "\n";
    471 					}
    472 
    473 					break;
    474 
    475 				case '#':
    476 					switch ($f[$j]) {
    477 						case '1':
    478 							$f[$j] = 'if';
    479 							break 2;
    480 						case '2':
    481 							$f[$j] = 'for';
    482 							break 2;
    483 						case '3':
    484 							$f[$j] = 'while';
    485 							break 2;
    486 					}
    487 
    488 				case '[';
    489 					if ($j >= 0 && "\n" == $f[$j]) {
    490 						$f[$j] = ';';
    491 					}
    492 
    493 				default:
    494 					$f[++$j] = $code[$i];
    495 			}
    496 
    497 			unset($instrPool[$s]);
    498 		}
    499 
    500 		$f = implode('', $f);
    501 		$cc_on && $f = str_replace('@#3', "\r", $f);
    502 
    503 		// Fix "else ;" empty instructions
    504 		$f = preg_replace("'(?<![\$.a-zA-Z0-9_])else\n'", "\n", $f);
    505 
    506 		$r1 = array( // keywords with a direct object
    507 			'case', 'delete', 'do', 'else', 'function', 'in', 'instanceof', 'of', 'break',
    508 			'new', 'return', 'throw', 'typeof', 'var', 'void', 'yield', 'let', 'if',
    509 			'const', 'get', 'set',
    510 		);
    511 
    512 		$r2 = array( // keywords with a subject
    513 			'in', 'instanceof', 'of',
    514 		);
    515 
    516 		// Fix missing semi-colons
    517 		$f = preg_replace("'(?<!(?<![a-zA-Z0-9_\$])" . implode(')(?<!(?<![a-zA-Z0-9_\$])', $r1) . ') (?!(' . implode('|', $r2) . ")(?![a-zA-Z0-9_\$]))'", "\n", $f);
    518 		$f = preg_replace("'(?<!(?<![a-zA-Z0-9_\$])do)(?<!(?<![a-zA-Z0-9_\$])else) if\('", "\nif(", $f);
    519 		$f = preg_replace("'(?<=--|\+\+)(?<![a-zA-Z0-9_\$])(" . implode('|', $r1) . ")(?![a-zA-Z0-9_\$])'", "\n$1", $f);
    520 		$f = preg_replace("'(?<![a-zA-Z0-9_\$])for\neach\('", 'for each(', $f);
    521 		$f = preg_replace("'(?<![a-zA-Z0-9_\$])\n(" . implode('|', $r2) . ")(?![a-zA-Z0-9_\$])'", '$1', $f);
    522 
    523 		// Merge strings
    524 		if ($q["'"] > $q['"']) {
    525 			$q = array($q[1], $q[0]);
    526 		}
    527 		$f = preg_replace("#//''\"\"[0-9]+'#", $q[0] . '$0' . $q[0], $f);
    528 		strpos($f, $q[0] . '+' . $q[0]) && $f = str_replace($q[0] . '+' . $q[0], '', $f);
    529 		$len = count($strings);
    530 		foreach ($strings as $r1 => &$r2) {
    531 			$r2 = "/'" == substr($r1, -2)
    532 				? str_replace(array("\\'", '\\"'), array("'", '"'), $r2)
    533 				: str_replace('\\' . $q[1], $q[1], $r2);
    534 		}
    535 
    536 		// Restore wanted spaces
    537 		$f = strtr($f, "\x7F", ' ');
    538 
    539 		return array($f, $strings);
    540 	}
    541 
    542 	protected function extractClosures($code)
    543 	{
    544 		$code = ';' . $code;
    545 
    546 		$this->argFreq[-1] += substr_count($code, '}catch(');
    547 
    548 		if ($this->argFreq[-1]) {
    549 			// Special catch scope handling
    550 
    551 			// FIXME: this implementation doesn't work with nested catch scopes who need
    552 			// access to their parent's caught variable (but who needs that?).
    553 
    554 			$f = preg_split("@}catch\(({$this->varRx})@", $code, -1, PREG_SPLIT_DELIM_CAPTURE);
    555 
    556 			$code = 'catch$scope$var' . mt_rand();
    557 			$this->specialVarRx = $this->specialVarRx ? '(?:' . $this->specialVarRx . '|' . preg_quote($code) . ')' : preg_quote($code);
    558 			$i = count($f) - 1;
    559 
    560 			while ($i) {
    561 				$c = 1;
    562 				$j = 0;
    563 				$l = strlen($f[$i]);
    564 
    565 				while ($c && $j < $l) {
    566 					$s = $f[$i][$j++];
    567 					$c += '(' == $s ? 1 : (')' == $s ? -1 : 0);
    568 				}
    569 
    570 				if (!$c) {
    571 					do {
    572 						$s = $f[$i][$j++];
    573 						$c += '{' == $s ? 1 : ('}' == $s ? -1 : 0);
    574 					} while ($c && $j < $l);
    575 				}
    576 
    577 				$c = preg_quote($f[$i - 1], '#');
    578 				$f[$i - 2] .= '}catch(' . preg_replace("#([.,{]?)(?<![a-zA-Z0-9_\$@]){$c}\\b#", '$1' . $code, $f[$i - 1] . substr($f[$i], 0, $j)) . substr($f[$i], $j);
    579 
    580 				unset($f[$i--], $f[$i--]);
    581 			}
    582 
    583 			$code = $f[0];
    584 		}
    585 
    586 		$f = preg_split("'(?<![a-zA-Z0-9_\$])((?:function[ (]|get |set ).*?\{)'", $code, -1, PREG_SPLIT_DELIM_CAPTURE);
    587 		$i = count($f) - 1;
    588 		$closures = array();
    589 
    590 		while ($i) {
    591 			$c = 1;
    592 			$j = 0;
    593 			$l = strlen($f[$i]);
    594 
    595 			while ($c && $j < $l) {
    596 				$s = $f[$i][$j++];
    597 				$c += '{' == $s ? 1 : ('}' == $s ? -1 : 0);
    598 			}
    599 
    600 			switch (substr($f[$i - 2], -1)) {
    601 				default:
    602 					if (false !== $c = strpos($f[$i - 1], ' ', 8)) {
    603 						break;
    604 					}
    605 				case false:
    606 				case "\n":
    607 				case ';':
    608 				case '{':
    609 				case '}':
    610 				case ')':
    611 				case ']':
    612 					$c = strpos($f[$i - 1], '(', 4);
    613 			}
    614 
    615 			$l = "//''\"\"#$i'";
    616 			$code = substr($f[$i - 1], $c);
    617 			$closures[$l] = $code . substr($f[$i], 0, $j);
    618 			$f[$i - 2] .= substr($f[$i - 1], 0, $c) . $l . substr($f[$i], $j);
    619 
    620 			if ('(){' !== $code) {
    621 				$j = substr_count($code, ',');
    622 				do {
    623 					isset($this->argFreq[$j]) ? ++$this->argFreq[$j] : $this->argFreq[$j] = 1;
    624 				} while ($j--);
    625 			}
    626 
    627 			$i -= 2;
    628 		}
    629 
    630 		return array($f[0], $closures);
    631 	}
    632 
    633 	protected function makeVars($closure, &$tree, $key)
    634 	{
    635 		$tree['code'] = &$closure;
    636 		$tree['nfe'] = false;
    637 		$tree['used'] = array();
    638 		$tree['local'] = array();
    639 
    640 		// Replace multiple "var" declarations by a single one
    641 		$closure = preg_replace_callback("'(?<=[\n\{\}])var [^\n\{\};]+(?:\nvar [^\n\{\};]+)+'", array($this, 'mergeVarDeclarations'), $closure);
    642 
    643 		// Get all local vars (functions, arguments and "var" prefixed)
    644 
    645 		$vars = &$tree['local'];
    646 
    647 		if (preg_match("'^( [^(]*)?\((.*?)\)\{'", $closure, $v)) {
    648 			if ($v[1]) {
    649 				$vars[$tree['nfe'] = substr($v[1], 1)] = -1;
    650 				$tree['parent']['local'][';' . $key] = &$vars[$tree['nfe']];
    651 			}
    652 
    653 			if ($v[2]) {
    654 				$i = 0;
    655 				$v = explode(',', $v[2]);
    656 				foreach ($v as $w) {
    657 					$vars[$w] = $this->argFreq[$i++] - 1; // Give a bonus to argument variables
    658 				}
    659 			}
    660 		}
    661 
    662 		$v = preg_split("'(?<![\$.a-zA-Z0-9_])var '", $closure);
    663 		if ($i = count($v) - 1) {
    664 			$w = array();
    665 
    666 			while ($i) {
    667 				$j = $c = 0;
    668 				$l = strlen($v[$i]);
    669 
    670 				while ($j < $l) {
    671 					switch ($v[$i][$j]) {
    672 						case '(':
    673 						case '[':
    674 						case '{':
    675 							++$c;
    676 							break;
    677 
    678 						case ')':
    679 						case ']':
    680 						case '}':
    681 							if ($c-- <= 0) {
    682 								break 2;
    683 							}
    684 							break;
    685 
    686 						case ';':
    687 						case "\n":
    688 							if (!$c) {
    689 								break 2;
    690 							}
    691 
    692 						default:
    693 							$c || $w[] = $v[$i][$j];
    694 					}
    695 
    696 					++$j;
    697 				}
    698 
    699 				$w[] = ',';
    700 				--$i;
    701 			}
    702 
    703 			$v = explode(',', implode('', $w));
    704 			foreach ($v as $w) {
    705 				if (preg_match("'^{$this->varRx}'", $w, $v)) {
    706 					isset($vars[$v[0]]) || $vars[$v[0]] = 0;
    707 				}
    708 			}
    709 		}
    710 
    711 		if (preg_match_all("@function ({$this->varRx})//''\"\"#@", $closure, $v)) {
    712 			foreach ($v[1] as $w) {
    713 				isset($vars[$w]) || $vars[$w] = 0;
    714 			}
    715 		}
    716 
    717 		if ($this->argFreq[-1] && preg_match_all("@}catch\(({$this->varRx})@", $closure, $v)) {
    718 			$v[0] = array();
    719 			foreach ($v[1] as $w) {
    720 				isset($v[0][$w]) ? ++$v[0][$w] : $v[0][$w] = 1;
    721 			}
    722 			foreach ($v[0] as $w => $v) {
    723 				$vars[$w] = $this->argFreq[-1] - $v;
    724 			}
    725 		}
    726 
    727 		// Get all used vars, local and non-local
    728 
    729 		$vars = &$tree['used'];
    730 
    731 		if (preg_match_all("#([.,{]?(?:[gs]et )?)(?<![a-zA-Z0-9_\$])({$this->varRx})(:?)#", $closure, $w, PREG_SET_ORDER)) {
    732 			foreach ($w as $k) {
    733 				if (isset($k[1][0]) && (',' === $k[1][0] || '{' === $k[1][0])) {
    734 					if (':' === $k[3]) {
    735 						$k = '.' . $k[2];
    736 					} elseif ('get ' === substr($k[1], 1, 4) || 'set ' === substr($k[1], 1, 4)) {
    737 						++$this->charFreq[ord($k[1][1])]; // "g" or "s"
    738 						++$this->charFreq[101]; // "e"
    739 						++$this->charFreq[116]; // "t"
    740 						$k = '.' . $k[2];
    741 					} else {
    742 						$k = $k[2];
    743 					}
    744 				} else {
    745 					$k = $k[1] . $k[2];
    746 				}
    747 
    748 				isset($vars[$k]) ? ++$vars[$k] : $vars[$k] = 1;
    749 			}
    750 		}
    751 
    752 		if (preg_match_all("#//''\"\"[0-9]+(?:['!]|/')#", $closure, $w)) {
    753 			foreach ($w[0] as $a) {
    754 				$v = "'" === substr($a, -1) && "/'" !== substr($a, -2) && $this->specialVarRx
    755 					? preg_split("#([.,{]?(?:[gs]et )?(?<![a-zA-Z0-9_\$@]){$this->specialVarRx}:?)#", $this->strings[$a], -1, PREG_SPLIT_DELIM_CAPTURE)
    756 					: array($this->strings[$a]);
    757 				$a = count($v);
    758 
    759 				for ($i = 0; $i < $a; ++$i) {
    760 					$k = $v[$i];
    761 
    762 					if (1 === $i % 2) {
    763 						if (',' === $k[0] || '{' === $k[0]) {
    764 							if (':' === substr($k, -1)) {
    765 								$k = '.' . substr($k, 1, -1);
    766 							} elseif ('get ' === substr($k, 1, 4) || 'set ' === substr($k, 1, 4)) {
    767 								++$this->charFreq[ord($k[1])]; // "g" or "s"
    768 								++$this->charFreq[101]; // "e"
    769 								++$this->charFreq[116]; // "t"
    770 								$k = '.' . substr($k, 5);
    771 							} else {
    772 								$k = substr($k, 1);
    773 							}
    774 						} elseif (':' === substr($k, -1)) {
    775 							$k = substr($k, 0, -1);
    776 						}
    777 
    778 						$w = &$tree;
    779 
    780 						while (isset($w['parent']) && !(isset($w['used'][$k]) || isset($w['local'][$k]))) {
    781 							$w = &$w['parent'];
    782 						}
    783 
    784 						(isset($w['used'][$k]) || isset($w['local'][$k])) && (isset($vars[$k]) ? ++$vars[$k] : $vars[$k] = 1);
    785 
    786 						unset($w);
    787 					}
    788 
    789 					if (0 === $i % 2 || !isset($vars[$k])) {
    790 						foreach (count_chars($v[$i], 1) as $k => $w) {
    791 							$this->charFreq[$k] += $w;
    792 						}
    793 					}
    794 				}
    795 			}
    796 		}
    797 
    798 		// Propagate the usage number to parents
    799 
    800 		foreach ($vars as $w => $a) {
    801 			$k = &$tree;
    802 			$chain = array();
    803 			do {
    804 				$vars = &$k['local'];
    805 				$chain[] = &$k;
    806 				if (isset($vars[$w])) {
    807 					unset($k['used'][$w]);
    808 					if (isset($vars[$w])) {
    809 						$vars[$w] += $a;
    810 					} else {
    811 						$vars[$w] = $a;
    812 					}
    813 					$a = false;
    814 					break;
    815 				}
    816 			} while ($k['parent'] && $k = &$k['parent']);
    817 
    818 			if ($a && !$k['parent']) {
    819 				if (isset($vars[$w])) {
    820 					$vars[$w] += $a;
    821 				} else {
    822 					$vars[$w] = $a;
    823 				}
    824 			}
    825 
    826 			if (isset($tree['used'][$w]) && isset($vars[$w])) {
    827 				foreach ($chain as &$b) {
    828 					isset($b['local'][$w]) || $b['used'][$w] = &$vars[$w];
    829 				}
    830 			}
    831 		}
    832 
    833 		// Analyse childs
    834 
    835 		$tree['childs'] = array();
    836 		$vars = &$tree['childs'];
    837 
    838 		if (preg_match_all("@//''\"\"#[0-9]+'@", $closure, $w)) {
    839 			foreach ($w[0] as $a) {
    840 				$vars[$a] = array('parent' => &$tree);
    841 				$this->makeVars($this->closures[$a], $vars[$a], $a);
    842 			}
    843 		}
    844 	}
    845 
    846 	protected function mergeVarDeclarations($m)
    847 	{
    848 		return str_replace("\nvar ", ',', $m[0]);
    849 	}
    850 
    851 	protected function renameVars(&$tree, $root)
    852 	{
    853 		if ($root) {
    854 			$tree['local'] += $tree['used'];
    855 			$tree['used'] = array();
    856 
    857 			foreach ($tree['local'] as $k => $v) {
    858 				if ('.' == $k[0]) {
    859 					$k = substr($k, 1);
    860 				}
    861 
    862 				if ('true' === $k) {
    863 					$this->charFreq[48] += $v;
    864 				} elseif ('false' === $k) {
    865 					$this->charFreq[49] += $v;
    866 				} elseif (!$this->specialVarRx || !preg_match("#^{$this->specialVarRx}$#", $k)) {
    867 					foreach (count_chars($k, 1) as $k => $w) {
    868 						$this->charFreq[$k] += $w * $v;
    869 					}
    870 				} elseif (2 == strlen($k)) {
    871 					$tree['used'][] = $k[1];
    872 				}
    873 			}
    874 
    875 			$this->charFreq = $this->rsort($this->charFreq);
    876 
    877 			$this->str0 = '';
    878 			$this->str1 = '';
    879 
    880 			foreach ($this->charFreq as $k => $v) {
    881 				if (!$v) {
    882 					break;
    883 				}
    884 
    885 				$v = chr($k);
    886 
    887 				if ((64 < $k && $k < 91) || (96 < $k && $k < 123)) { // A-Z a-z
    888 					$this->str0 .= $v;
    889 					$this->str1 .= $v;
    890 				} elseif (47 < $k && $k < 58) { // 0-9
    891 					$this->str1 .= $v;
    892 				}
    893 			}
    894 
    895 			if ('' === $this->str0) {
    896 				$this->str0 = 'claspemitdbfrugnjvhowkxqyzCLASPEMITDBFRUGNJVHOWKXQYZ';
    897 				$this->str1 = $this->str0 . '0123456789';
    898 			}
    899 
    900 			foreach ($tree['local'] as $var => $root) {
    901 				if ('.' != substr($var, 0, 1) && isset($tree['local'][".{$var}"])) {
    902 					$tree['local'][$var] += $tree['local'][".{$var}"];
    903 				}
    904 			}
    905 
    906 			foreach ($tree['local'] as $var => $root) {
    907 				if ('.' == substr($var, 0, 1) && isset($tree['local'][substr($var, 1)])) {
    908 					$tree['local'][$var] = $tree['local'][substr($var, 1)];
    909 				}
    910 			}
    911 
    912 			$tree['local'] = $this->rsort($tree['local']);
    913 
    914 			foreach ($tree['local'] as $var => $root) {
    915 				switch (substr($var, 0, 1)) {
    916 					case '.':
    917 						if (!isset($tree['local'][substr($var, 1)])) {
    918 							$tree['local'][$var] = '#' . ($this->specialVarRx && 3 < strlen($var) && preg_match("'^\.{$this->specialVarRx}$'", $var) ? $this->getNextName($tree) . '$' : substr($var, 1));
    919 						}
    920 						break;
    921 
    922 					case ';':
    923 						$tree['local'][$var] = 0 === $root ? '' : $this->getNextName($tree);
    924 					case '#':
    925 						break;
    926 
    927 					default:
    928 						$root = $this->specialVarRx && 2 < strlen($var) && preg_match("'^{$this->specialVarRx}$'", $var) ? $this->getNextName($tree) . '$' : $var;
    929 						$tree['local'][$var] = $root;
    930 						if (isset($tree['local'][".{$var}"])) {
    931 							$tree['local'][".{$var}"] = '#' . $root;
    932 						}
    933 				}
    934 			}
    935 
    936 			foreach ($tree['local'] as $var => $root) {
    937 				$tree['local'][$var] = preg_replace("'^#'", '.', $tree['local'][$var]);
    938 			}
    939 		} else {
    940 			$tree['local'] = $this->rsort($tree['local']);
    941 			if (false !== $tree['nfe']) {
    942 				$tree['used'][] = $tree['local'][$tree['nfe']];
    943 			}
    944 
    945 			foreach ($tree['local'] as $var => $root) {
    946 				if ($tree['nfe'] !== $var) {
    947 					$tree['local'][$var] = 0 === $root ? '' : $this->getNextName($tree);
    948 				}
    949 			}
    950 		}
    951 
    952 		$this->local_tree = &$tree['local'];
    953 		$this->used_tree = &$tree['used'];
    954 
    955 		$tree['code'] = preg_replace_callback("#[.,{ ]?(?:[gs]et )?(?<![a-zA-Z0-9_\$@]){$this->varRx}:?#", array($this, 'getNewName'), $tree['code']);
    956 
    957 		if ($this->specialVarRx && preg_match_all("#//''\"\"[0-9]+'#", $tree['code'], $b)) {
    958 			foreach ($b[0] as $a) {
    959 				$this->strings[$a] = preg_replace_callback(
    960 					"#[.,{]?(?:[gs]et )?(?<![a-zA-Z0-9_\$@]){$this->specialVarRx}:?#",
    961 					array($this, 'getNewName'),
    962 					$this->strings[$a]
    963 				);
    964 			}
    965 		}
    966 
    967 		foreach ($tree['childs'] as $a => &$b) {
    968 			$this->renameVars($b, false);
    969 			$tree['code'] = str_replace($a, $b['code'], $tree['code']);
    970 			unset($tree['childs'][$a]);
    971 		}
    972 	}
    973 
    974 	protected function getNewName($m)
    975 	{
    976 		$m = $m[0];
    977 
    978 		$pre = '.' === $m[0] ? '.' : '';
    979 		$post = '';
    980 
    981 		if (',' === $m[0] || '{' === $m[0] || ' ' === $m[0]) {
    982 			$pre = $m[0];
    983 
    984 			if (':' === substr($m, -1)) {
    985 				$post = ':';
    986 				$m = (' ' !== $m[0] ? '.' : '') . substr($m, 1, -1);
    987 			} elseif ('get ' === substr($m, 1, 4) || 'set ' === substr($m, 1, 4)) {
    988 				$pre .= substr($m, 1, 4);
    989 				$m = '.' . substr($m, 5);
    990 			} else {
    991 				$m = substr($m, 1);
    992 			}
    993 		} elseif (':' === substr($m, -1)) {
    994 			$post = ':';
    995 			$m = substr($m, 0, -1);
    996 		}
    997 
    998 		$post = (isset($this->reserved[$m])
    999 				? ('true' === $m ? '!0' : ('false' === $m ? '!1' : $m))
   1000 				: (
   1001 				isset($this->local_tree[$m])
   1002 					? $this->local_tree[$m]
   1003 					: (
   1004 				isset($this->used_tree[$m])
   1005 					? $this->used_tree[$m]
   1006 					: $m
   1007 				)
   1008 				)
   1009 			) . $post;
   1010 
   1011 		return '' === $post ? '' : ($pre . ('.' === $post[0] ? substr($post, 1) : $post));
   1012 	}
   1013 
   1014 	protected function getNextName(&$tree = array(), &$counter = false)
   1015 	{
   1016 		if (false === $counter) {
   1017 			$counter = &$tree['counter'];
   1018 			isset($counter) || $counter = -1;
   1019 			$exclude = array_flip($tree['used']);
   1020 		} else {
   1021 			$exclude = $tree;
   1022 		}
   1023 
   1024 		++$counter;
   1025 
   1026 		$len0 = strlen($this->str0);
   1027 		$len1 = strlen($this->str0);
   1028 
   1029 		$name = $this->str0[$counter % $len0];
   1030 
   1031 		$i = intval($counter / $len0) - 1;
   1032 		while ($i >= 0) {
   1033 			$name .= $this->str1[$i % $len1];
   1034 			$i = intval($i / $len1) - 1;
   1035 		}
   1036 
   1037 		return !(isset($this->reserved[$name]) || isset($exclude[$name])) ? $name : $this->getNextName($exclude, $counter);
   1038 	}
   1039 
   1040 	protected function restoreCc(&$s, $lf = true)
   1041 	{
   1042 		$lf && $s = str_replace('@#3', '', $s);
   1043 
   1044 		$s = str_replace('@#1', '@*/', $s);
   1045 		$s = str_replace('2#@', '//@', $s);
   1046 		$s = str_replace('1#@', '/*@', $s);
   1047 		$s = str_replace('##', '#', $s);
   1048 	}
   1049 
   1050 	private function rsort($array)
   1051 	{
   1052 		if (!$array) {
   1053 			return $array;
   1054 		}
   1055 
   1056 		$i = 0;
   1057 		$tuples = array();
   1058 		foreach ($array as $k => &$v) {
   1059 			$tuples[] = array(++$i, $k, &$v);
   1060 		}
   1061 
   1062 		usort($tuples, function ($a, $b) {
   1063 			if ($b[2] > $a[2]) {
   1064 				return 1;
   1065 			}
   1066 			if ($b[2] < $a[2]) {
   1067 				return -1;
   1068 			}
   1069 			if ($b[0] > $a[0]) {
   1070 				return -1;
   1071 			}
   1072 			if ($b[0] < $a[0]) {
   1073 				return 1;
   1074 			}
   1075 
   1076 			return 0;
   1077 		});
   1078 
   1079 		$array = array();
   1080 
   1081 		foreach ($tuples as $t) {
   1082 			$array[$t[1]] = &$t[2];
   1083 		}
   1084 
   1085 		return $array;
   1086 	}
   1087 }
   1088 
   1089 ?>