Ticket #781: min-js.php

File min-js.php, 32.0 KB (added by Volker Strähle, 10 years ago)

includes/external/minjs javascript compressor

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