File modules/util/JSqueeze.class.php

Last commit: Sat Feb 22 23:58:02 2020 +0100	Jan Dankert	Refactoring: Namespacing for module 'util'.
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 ?>
Download modules/util/JSqueeze.class.php
History Sat, 22 Feb 2020 23:58:02 +0100 Jan Dankert Refactoring: Namespacing for module 'util'. Sat, 16 Dec 2017 23:21:31 +0100 Jan Dankert Eigenes Modul für alle Util-Klassen.