From 5b6469467ef72496bfdfe74a7d7b0bb33490074f Mon Sep 17 00:00:00 2001 From: Steve Clay Date: Sun, 26 Jun 2011 02:37:00 -0400 Subject: [PATCH] proper fix for Issue 144: + ++a --- min/lib/JSMin.php | 51 ++++++++++++------- min_unit_tests/_test_files/js/issue144.js | 11 +++- min_unit_tests/_test_files/js/issue144.min.js | 3 +- min_unit_tests/test_JSMin.php | 11 ++-- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/min/lib/JSMin.php b/min/lib/JSMin.php index 011eca3..226431e 100644 --- a/min/lib/JSMin.php +++ b/min/lib/JSMin.php @@ -1,20 +1,20 @@ * $minifiedJs = JSMin::minify($js); * * - * This is a direct port of jsmin.c to PHP with a few PHP performance tweaks and - * modifications to preserve some comments (see below). Also, rather than using - * stdin/stdout, JSMin::minify() accepts a string as input and returns another - * string as output. - * - * Comments containing IE conditional compilation are preserved, as are multi-line - * comments that begin with "/*!" (for documentation purposes). In the latter case - * newlines are inserted around the comment to enhance readability. + * This is a modified port of jsmin.c. Improvements: + * + * Does not choke on some regexp literals containing quote characters. E.g. /'/ + * + * Spaces are preserved after some add/sub operators, so they are not mistakenly + * converted to post-inc/dec. E.g. a + ++b -> a+ ++b * + * Preserves multi-line comments that begin with /*! + * * PHP 5 or higher is required. * * Permission is hereby granted to use this version of the library under the @@ -68,6 +68,7 @@ class JSMin { protected $inputLength = 0; protected $lookAhead = null; protected $output = ''; + protected $lastByteOut = ''; /** * Minify Javascript. @@ -87,13 +88,6 @@ class JSMin { public function __construct($input) { $this->input = $input; - // look out for syntax like "++ +" and "- ++" - $p = '\\+'; - $m = '\\-'; - if (preg_match("/([$p$m])(?:\\1 [$p$m]| (?:$p$p|$m$m))/", $input)) { - // likely pre-minified and would be broken by JSMin - $this->output = $input; - } } /** @@ -119,7 +113,11 @@ class JSMin { // determine next command $command = self::ACTION_KEEP_A; // default if ($this->a === ' ') { - if (! $this->isAlphaNum($this->b)) { + if (($this->lastByteOut === '+' || $this->lastByteOut === '-') + && ($this->b === $this->lastByteOut)) { + // Don't delete this space. If we do, the addition/subtraction + // could be parsed as a post-increment + } elseif (! $this->isAlphaNum($this->b)) { $command = self::ACTION_DELETE_A; } } elseif ($this->a === "\n") { @@ -134,7 +132,7 @@ class JSMin { } } elseif (! $this->isAlphaNum($this->a)) { if ($this->b === ' ' - || ($this->b === "\n" + || ($this->b === "\n" && (false === strpos('}])+-"\'', $this->a)))) { $command = self::ACTION_DELETE_A_B; } @@ -156,9 +154,21 @@ class JSMin { */ protected function action($command) { + if ($command === self::ACTION_DELETE_A_B + && $this->b === ' ' + && ($this->a === '+' || $this->a === '-')) { + // Note: we're at an addition/substraction operator; the inputIndex + // will certainly be a valid index + if ($this->input[$this->inputIndex] === $this->a) { + // This is "+ +" or "- -". Don't delete the space. + $command = self::ACTION_KEEP_A; + } + } switch ($command) { case self::ACTION_KEEP_A: $this->output .= $this->a; + $this->lastByteOut = $this->a; + // fallthrough case self::ACTION_DELETE_A: $this->a = $this->b; @@ -166,6 +176,8 @@ class JSMin { $str = $this->a; // in case needed for exception while (true) { $this->output .= $this->a; + $this->lastByteOut = $this->a; + $this->a = $this->get(); if ($this->a === $this->b) { // end quote break; @@ -178,6 +190,8 @@ class JSMin { $str .= $this->a; if ($this->a === '\\') { $this->output .= $this->a; + $this->lastByteOut = $this->a; + $this->a = $this->get(); $str .= $this->a; } @@ -204,6 +218,7 @@ class JSMin { . $this->inputIndex .": {$pattern}"); } $this->output .= $this->a; + $this->lastByteOut = $this->a; } $this->b = $this->next(); } diff --git a/min_unit_tests/_test_files/js/issue144.js b/min_unit_tests/_test_files/js/issue144.js index 41b72f1..ba1b297 100644 --- a/min_unit_tests/_test_files/js/issue144.js +++ b/min_unit_tests/_test_files/js/issue144.js @@ -1,2 +1,9 @@ -// JSMin should not alter this file -if(!a.id)a.id="dp"+ ++this.uuid; +a / ++b; +a * --b; +a++ - b; +a + --b; +a - ++b; +a + -b; +a + ++b; +a + --b; +a - --b; \ No newline at end of file diff --git a/min_unit_tests/_test_files/js/issue144.min.js b/min_unit_tests/_test_files/js/issue144.min.js index 41b72f1..e339d0a 100644 --- a/min_unit_tests/_test_files/js/issue144.min.js +++ b/min_unit_tests/_test_files/js/issue144.min.js @@ -1,2 +1 @@ -// JSMin should not alter this file -if(!a.id)a.id="dp"+ ++this.uuid; +a/++b;a*--b;a++-b;a+--b;a-++b;a+-b;a+ ++b;a+--b;a- --b; \ No newline at end of file diff --git a/min_unit_tests/test_JSMin.php b/min_unit_tests/test_JSMin.php index ef3a978..f8087c8 100644 --- a/min_unit_tests/test_JSMin.php +++ b/min_unit_tests/test_JSMin.php @@ -6,7 +6,7 @@ require_once 'JSMin.php'; function test_JSMin() { global $thisDir; - + $src = file_get_contents($thisDir . '/_test_files/js/before.js'); $minExpected = file_get_contents($thisDir . '/_test_files/js/before.min.js'); $minOutput = JSMin::minify($src); @@ -16,11 +16,16 @@ function test_JSMin() echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; } - + $src = file_get_contents($thisDir . '/_test_files/js/issue144.js'); $minExpected = file_get_contents($thisDir . '/_test_files/js/issue144.min.js'); $minOutput = JSMin::minify($src); - $passed = assertTrue($minExpected == $minOutput, 'JSMin : Don\'t minify files with + ++ (Issue 144)'); + $passed = assertTrue($minExpected == $minOutput, 'JSMin : Handle "+ ++a" syntax (Issue 144)'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Output: " .countBytes($minOutput). " bytes\n\n{$minOutput}\n\n"; + echo "---Expected: " .countBytes($minExpected). " bytes\n\n{$minExpected}\n\n"; + echo "---Source: " .countBytes($src). " bytes\n\n{$src}\n\n\n"; + } if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { $src = file_get_contents($thisDir . '/_test_files/js/issue132.js');