Procházet zdrojové kódy

#298 Update Matthiasmullie Minify/Path-converted from 1.3.28/1.0.5 to 1.3.35/1.0.8

Alex Trofimov před 10 roky
rodič
revize
ddec44fcb2

+ 18 - 0
plugins/matthiasmullie/LICENSE

@@ -0,0 +1,18 @@
+Copyright (c) 2015 Matthias Mullie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 182 - 47
plugins/matthiasmullie/minify/CSS.php

@@ -1,6 +1,8 @@
 <?php
+
 namespace MatthiasMullie\Minify;
 
+use MatthiasMullie\Minify\Exceptions\FileImportException;
 use MatthiasMullie\PathConverter\Converter;
 
 /**
@@ -10,7 +12,6 @@ use MatthiasMullie\PathConverter\Converter;
  *
  * @author Matthias Mullie <minify@mullie.eu>
  * @author Tijs Verkoyen <minify@verkoyen.eu>
- *
  * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved.
  * @license MIT License
  */
@@ -27,10 +28,14 @@ class CSS extends Minify
     protected $importExtensions = array(
         'gif' => 'data:image/gif',
         'png' => 'data:image/png',
-        'jpg' => 'data:image/jpg',
+        'jpe' => 'data:image/jpeg',
+        'jpg' => 'data:image/jpeg',
         'jpeg' => 'data:image/jpeg',
         'svg' => 'data:image/svg+xml',
         'woff' => 'data:application/x-font-woff',
+        'tif' => 'image/tiff',
+        'tiff' => 'image/tiff',
+        'xbm' => 'image/x-xbitmap',
     );
 
     /**
@@ -61,16 +66,43 @@ class CSS extends Minify
         $this->importExtensions = $extensions;
     }
 
+    /**
+     * Move any import statements to the top.
+     *
+     * @param string $content Nearly finished CSS content
+     *
+     * @return string
+     */
+    protected function moveImportsToTop($content)
+    {
+        if (preg_match_all('/@import[^;]+;/', $content, $matches)) {
+            // remove from content
+            foreach ($matches[0] as $import) {
+                $content = str_replace($import, '', $content);
+            }
+
+            // add to top
+            $content = implode('', $matches[0]).$content;
+        };
+
+        return $content;
+    }
+
     /**
      * Combine CSS from import statements.
+     *
      * @import's will be loaded and their content merged into the original file,
      * to save HTTP requests.
      *
-     * @param  string $source  The file to combine imports for.
-     * @param  string $content The CSS content to combine imports for.
+     * @param string   $source  The file to combine imports for.
+     * @param string   $content The CSS content to combine imports for.
+     * @param string[] $parents Parent paths, for circular reference checks.
+     *
      * @return string
+     *
+     * @throws FileImportException
      */
-    protected function combineImports($source, $content)
+    protected function combineImports($source, $content, $parents)
     {
         $importRegexes = array(
             // @import url(xxx)
@@ -179,14 +211,20 @@ class CSS extends Minify
 
             // only replace the import with the content if we can grab the
             // content of the file
-            if (@file_exists($importPath) && is_file($importPath)) {
+            if ($this->canImportFile($importPath)) {
+                // check if current file was not imported previously in the same
+                // import chain.
+                if (in_array($importPath, $parents)) {
+                    throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.');
+                }
+
                 // grab referenced file & minify it (which may include importing
                 // yet other @import statements recursively)
                 $minifier = new static($importPath);
-                $importContent = $minifier->execute($source);
+                $importContent = $minifier->execute($source, $parents);
 
                 // check if this is only valid for certain media
-                if ($match['media']) {
+                if (!empty($match['media'])) {
                     $importContent = '@media '.$match['media'].'{'.$importContent.'}';
                 }
 
@@ -204,11 +242,13 @@ class CSS extends Minify
 
     /**
      * Import files into the CSS, base64-ized.
+     *
      * @url(image.jpg) images will be loaded and their content merged into the
      * original file, to save HTTP requests.
      *
-     * @param  string $source  The file to import files for.
-     * @param  string $content The CSS content to import files for.
+     * @param string $source  The file to import files for.
+     * @param string $content The CSS content to import files for.
+     *
      * @return string
      */
     protected function importFiles($source, $content)
@@ -228,20 +268,15 @@ class CSS extends Minify
 
                 // only replace the import with the content if we're able to get
                 // the content of the file, and it's relatively small
-                $import = @file_exists($path);
-                $import = $import && is_file($path);
-                $import = $import && filesize($path) <= $this->maxImportSize * 1024;
-                if (!$import) {
-                    continue;
+                if ($this->canImportFile($path) && $this->canImportBySize($path)) {
+                    // grab content && base64-ize
+                    $importContent = $this->load($path);
+                    $importContent = base64_encode($importContent);
+
+                    // build replacement
+                    $search[] = $match[0];
+                    $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
                 }
-
-                // grab content && base64-ize
-                $importContent = $this->load($path);
-                $importContent = base64_encode($importContent);
-
-                // build replacement
-                $search[] = $match[0];
-                $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
             }
 
             // replace the import statements
@@ -255,14 +290,16 @@ class CSS extends Minify
      * Minify the data.
      * Perform CSS optimizations.
      *
-     * @param  string[optional] $path Path to write the data to.
-     * @return string           The minified data.
+     * @param string[optional] $path    Path to write the data to.
+     * @param string[]         $parents Parent paths, for circular reference checks.
+     *
+     * @return string The minified data.
      */
-    public function execute($path = null)
+    public function execute($path = null, $parents = array())
     {
         $content = '';
 
-        // loop files
+        // loop css data (raw data and files)
         foreach ($this->data as $source => $css) {
             /*
              * Let's first take out strings & comments, since we can't just remove
@@ -277,10 +314,17 @@ class CSS extends Minify
             $css = $this->stripWhitespace($css);
             $css = $this->shortenHex($css);
             $css = $this->shortenZeroes($css);
+            $css = $this->shortenFontWeights($css);
+            $css = $this->stripEmptyTags($css);
 
             // restore the string we've extracted earlier
             $css = $this->restoreExtractedData($css);
 
+            $source = is_int($source) ? '' : $source;
+            $parents = $source ? array_merge($parents, array($source)) : $parents;
+            $css = $this->combineImports($source, $css, $parents);
+            $css = $this->importFiles($source, $css);
+
             /*
              * If we'll save to a new path, we'll have to fix the relative paths
              * to be relative no longer to the source file, but to the new path.
@@ -288,19 +332,15 @@ class CSS extends Minify
              * conversion happens (because we still want it to go through most
              * of the move code...)
              */
-            $source = $source ?: '';
             $converter = new Converter($source, $path ?: $source);
             $css = $this->move($converter, $css);
 
-            // if no target path is given, relative paths were not converted, so
-            // they'll still be relative to the source file then
-            $css = $this->importFiles($path ?: $source, $css);
-            $css = $this->combineImports($path ?: $source, $css);
-
             // combine css
             $content .= $css;
         }
 
+        $content = $this->moveImportsToTop($content);
+
         return $content;
     }
 
@@ -308,10 +348,11 @@ class CSS extends Minify
      * Moving a css file should update all relative urls.
      * Relative references (e.g. ../images/image.gif) in a certain css file,
      * will have to be updated when a file is being saved at another location
-     * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper)
+     * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
+     *
+     * @param Converter $converter Relative path converter
+     * @param string    $content   The CSS content to update relative urls for.
      *
-     * @param  Converter $converter Relative path converter
-     * @param  string    $content   The CSS content to update relative urls for.
      * @return string
      */
     protected function move(Converter $converter, $content)
@@ -409,8 +450,15 @@ class CSS extends Minify
             // determine if it's a url() or an @import match
             $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
 
+            // attempting to interpret GET-params makes no sense, so let's discard them for awhile
+            $params = strrchr($match['path'], '?');
+            $url = $params ? substr($match['path'], 0, -strlen($params)) : $match['path'];
+
             // fix relative url
-            $url = $converter->convert($match['path']);
+            $url = $converter->convert($url);
+
+            // now that the path has been converted, re-apply GET-params
+            $url .= $params;
 
             // build replacement
             $search[] = $match[0];
@@ -429,22 +477,76 @@ class CSS extends Minify
 
     /**
      * Shorthand hex color codes.
-     * #FF0000 -> #F00
+     * #FF0000 -> #F00.
+     *
+     * @param string $content The CSS content to shorten the hex color codes for.
      *
-     * @param  string $content The CSS content to shorten the hex color codes for.
      * @return string
      */
     protected function shortenHex($content)
     {
         $content = preg_replace('/(?<![\'"])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?![\'"])/i', '#$1$2$3', $content);
 
-        return $content;
+        // we can shorten some even more by replacing them with their color name
+        $colors = array(
+            '#F0FFFF' => 'azure',
+            '#F5F5DC' => 'beige',
+            '#A52A2A' => 'brown',
+            '#FF7F50' => 'coral',
+            '#FFD700' => 'gold',
+            '#808080' => 'gray',
+            '#008000' => 'green',
+            '#4B0082' => 'indigo',
+            '#FFFFF0' => 'ivory',
+            '#F0E68C' => 'khaki',
+            '#FAF0E6' => 'linen',
+            '#800000' => 'maroon',
+            '#000080' => 'navy',
+            '#808000' => 'olive',
+            '#CD853F' => 'peru',
+            '#FFC0CB' => 'pink',
+            '#DDA0DD' => 'plum',
+            '#800080' => 'purple',
+            '#F00' => 'red',
+            '#FA8072' => 'salmon',
+            '#A0522D' => 'sienna',
+            '#C0C0C0' => 'silver',
+            '#FFFAFA' => 'snow',
+            '#D2B48C' => 'tan',
+            '#FF6347' => 'tomato',
+            '#EE82EE' => 'violet',
+            '#F5DEB3' => 'wheat',
+        );
+
+        return str_ireplace(array_keys($colors), $colors, $content);
+    }
+
+    /**
+     * Shorten CSS font weights.
+     *
+     * @param string $content The CSS content to shorten the font weights for.
+     *
+     * @return string
+     */
+    protected function shortenFontWeights($content)
+    {
+        $weights = array(
+            'normal' => 400,
+            'bold' => 700,
+        );
+
+        $callback = function ($match) use ($weights) {
+            return $match[1] . $weights[$match[2]];
+        };
+
+        return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
     }
 
     /**
      * Shorthand 0 values to plain 0, instead of e.g. -0em.
      *
-     * @param  string $content The CSS content to shorten the zero values for.
+     * @param string $content The CSS content to shorten the zero values for.
+     *
      * @return string
      */
     protected function shortenZeroes($content)
@@ -459,19 +561,39 @@ class CSS extends Minify
         $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
 
         // strip units after zeroes (0px -> 0)
-        $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)'.$units.$after.'/', '\\1', $content);
+        // NOTE: it should be safe to remove all units for a 0 value, but in
+        // practice, Webkit (especially Safari) seems to stumble over at least
+        // 0%, potentially other units as well. Only stripping 'px' for now.
+        // @see https://github.com/matthiasmullie/minify/issues/60
+        $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
 
         // strip 0-digits (.0 -> 0)
-        $content = preg_replace('/'.$before.'\.0+'.$after.'/', '0', $content);
-        // 50.00 -> 50, 50.00px -> 50px (non-0 can still be followed by units)
+        $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
+        // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
+        $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
+        // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
         $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
+        // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
+        $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
 
         // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
-        $content = preg_replace('/'.$before.'-?0+'.$after.'/', '0', $content);
+        $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
 
         return $content;
     }
 
+    /**
+     * Strip comments from source code.
+     *
+     * @param string $content
+     *
+     * @return string
+     */
+    protected function stripEmptyTags($content)
+    {
+        return preg_replace('/(^|\})[^\{\}]+\{\s*\}/', '\\1', $content);
+    }
+
     /**
      * Strip comments from source code.
      */
@@ -483,7 +605,8 @@ class CSS extends Minify
     /**
      * Strip whitespace.
      *
-     * @param  string $content The CSS content to strip the whitespace for.
+     * @param string $content The CSS content to strip the whitespace for.
+     *
      * @return string
      */
     protected function stripWhitespace($content)
@@ -508,8 +631,20 @@ class CSS extends Minify
         $content = preg_replace('/\s*([+-])\s*(?=[^}]*{)/', '$1', $content);
 
         // remove semicolon/whitespace followed by closing bracket
-        $content = preg_replace('/;}/', '}', $content);
+        $content = str_replace(';}', '}', $content);
 
         return trim($content);
     }
+
+    /**
+     * Check if file is small enough to be imported.
+     *
+     * @param string $path The path to the file.
+     *
+     * @return bool
+     */
+    protected function canImportBySize($path)
+    {
+        return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
+    }
 }

+ 4 - 1
plugins/matthiasmullie/minify/Exception.php

@@ -1,9 +1,12 @@
 <?php
+
 namespace MatthiasMullie\Minify;
 
 /**
+ * @deprecated Use Exceptions\BasicException instead
+ *
  * @author Matthias Mullie <minify@mullie.eu>
  */
-class Exception extends \Exception
+abstract class Exception extends \Exception
 {
 }

+ 12 - 0
plugins/matthiasmullie/minify/Exceptions/BasicException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace MatthiasMullie\Minify\Exceptions;
+
+use MatthiasMullie\Minify\Exception;
+
+/**
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+abstract class BasicException extends Exception
+{
+}

+ 10 - 0
plugins/matthiasmullie/minify/Exceptions/FileImportException.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace MatthiasMullie\Minify\Exceptions;
+
+/**
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+class FileImportException extends BasicException
+{
+}

+ 10 - 0
plugins/matthiasmullie/minify/Exceptions/IOException.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace MatthiasMullie\Minify\Exceptions;
+
+/**
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+class IOException extends BasicException
+{
+}

+ 85 - 35
plugins/matthiasmullie/minify/JS.php

@@ -1,4 +1,5 @@
 <?php
+
 namespace MatthiasMullie\Minify;
 
 /**
@@ -8,14 +9,13 @@ namespace MatthiasMullie\Minify;
  *
  * @author Matthias Mullie <minify@mullie.eu>
  * @author Tijs Verkoyen <minify@verkoyen.eu>
- *
  * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved.
  * @license MIT License
  */
 class JS extends Minify
 {
     /**
-     * Var-matching regex based on http://stackoverflow.com/a/9337047/802993
+     * Var-matching regex based on http://stackoverflow.com/a/9337047/802993.
      *
      * Note that regular expressions using that bit must have the PCRE_UTF8
      * pattern modifier (/u) set.
@@ -26,9 +26,10 @@ class JS extends Minify
 
     /**
      * Full list of JavaScript reserved words.
-     * Will be loaded from /data/js/keywords_reserved.txt
+     * Will be loaded from /data/js/keywords_reserved.txt.
      *
      * @see https://mathiasbynens.be/notes/reserved-keywords
+     *
      * @var string[]
      */
     protected $keywordsReserved = array();
@@ -75,6 +76,7 @@ class JS extends Minify
      * Will be loaded from /data/js/operators_before.txt
      *
      * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators
+     *
      * @var string[]
      */
     protected $operatorsBefore = array();
@@ -92,12 +94,13 @@ class JS extends Minify
      * Will be loaded from /data/js/operators_after.txt
      *
      * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators
+     *
      * @var string[]
      */
     protected $operatorsAfter = array();
 
     /**
-     * {@inheritDoc}
+     * {@inheritdoc}
      */
     public function __construct()
     {
@@ -116,8 +119,9 @@ class JS extends Minify
      * Minify the data.
      * Perform JS optimizations.
      *
-     * @param  string[optional] $path Path to write the data to.
-     * @return string           The minified data.
+     * @param string[optional] $path Path to write the data to.
+     *
+     * @return string The minified data.
      */
     public function execute($path = null)
     {
@@ -178,7 +182,7 @@ class JS extends Minify
     }
 
     /**
-     * JS can have /-delimited regular expressions, like: /ab+c/.match(string)
+     * JS can have /-delimited regular expressions, like: /ab+c/.match(string).
      *
      * The content inside the regex can contain characters that may be confused
      * for JS code: e.g. it could contain whitespace it needs to match & we
@@ -201,18 +205,27 @@ class JS extends Minify
         $callback = function ($match) use ($minifier) {
             $count = count($minifier->extracted);
             $placeholder = '/'.$count.'/';
-            $minifier->extracted[$placeholder] = $match[1];
+            $minifier->extracted[$placeholder] = $match[0];
 
             return $placeholder;
         };
 
-        // it's a regex if we can find an opening (not preceded by variable,
-        // value or similar) & (non-escaped) closing /,
-        // include \n because it may be there for a reason
+        $pattern = '\/.*?(?<!\\\\)(\\\\\\\\)*+\/[gimy]*';
+
+        // a regular expression can only be followed by a few operators or some
+        // of the RegExp methods (a `\` followed by a variable or value is
+        // likely part of a division, not a regex)
+        $after = '[,;\)]';
+        $methods = '\.(exec|test|match|search|replace|split)\(';
+        $this->registerPattern('/'.$pattern.'\s*(?=('.$after.'|'.$methods.'))/', $callback);
+
+        // 1 more edgecase: a regex can be followed by a lot more operators or
+        // keywords if there's a newline (ASI) in between, where the operator
+        // actually starts a new statement
         // (https://github.com/matthiasmullie/minify/issues/56)
-        $this->registerPattern('/^\s*+\K(\/.*?(?<!\\\\)(\\\\\\\\)*+\/\n?)/', $callback);
-        $before = $this->getOperatorsForRegex($this->operatorsBefore, '/') + $this->getKeywordsForRegex($this->keywordsReserved, '/');
-        $this->registerPattern('/(?:'.implode('|', $before).')\s*+\K(\/.*?(?<!\\\\)(\\\\\\\\)*+\/\n?)/', $callback);
+        $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/');
+        $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/');
+        $this->registerPattern('/'.$pattern.'\s*\n?(?=\s*('.implode('|', $operators).'))/', $callback);
     }
 
     /**
@@ -227,7 +240,8 @@ class JS extends Minify
      * Because it's sometimes hard to tell if a newline is part of a statement
      * that should be terminated or not, we'll just leave some of them alone.
      *
-     * @param  string $content The content to strip the whitespace for.
+     * @param string $content The content to strip the whitespace for.
+     *
      * @return string
      */
     protected function stripWhitespace($content)
@@ -244,16 +258,27 @@ class JS extends Minify
         // collapse consecutive line feeds into just 1
         $content = preg_replace('/\n+/', "\n", $content);
 
-        // strip whitespace that ends in (or next line begin with) an operator
-        // that allows statements to be broken up over multiple lines
         $before = $this->getOperatorsForRegex($this->operatorsBefore, '/');
         $after = $this->getOperatorsForRegex($this->operatorsAfter, '/');
-        $content = preg_replace('/('.implode('|', $before).')\s+/', '\\1', $content);
-        $content = preg_replace('/\s+('.implode('|', $after).')/', '\\1', $content);
+        $operators = $before + $after;
+
+        // strip whitespace that ends in (or next line begin with) an operator
+        // that allows statements to be broken up over multiple lines
+        unset($before['+'], $before['-'], $after['+'], $after['-']);
+        $content = preg_replace(
+            array(
+                '/('.implode('|', $before).')\s+/',
+                '/\s+('.implode('|', $after).')/',
+            ), '\\1', $content
+        );
 
         // make sure + and - can't be mistaken for, or joined into ++ and --
-        $content = preg_replace('/(?<![\+\-])\s*([\+\-])(?![\+\-])/', '\\1', $content);
-        $content = preg_replace('/(?<![\+\-])([\+\-])\s*(?![\+\-])/', '\\1', $content);
+        $content = preg_replace(
+            array(
+                '/(?<![\+\-])\s*([\+\-])(?![\+\-])/',
+                '/(?<![\+\-])([\+\-])\s*(?![\+\-])/',
+            ), '\\1', $content
+        );
 
         /*
          * We didn't strip whitespace after a couple of operators because they
@@ -261,7 +286,6 @@ class JS extends Minify
          * strip the newlines. However, we can safely strip any non-line feed
          * whitespace that follows them.
          */
-        $operators = $this->getOperatorsForRegex($this->operatorsBefore + $this->operatorsAfter, '/');
         $content = preg_replace('/([\}\)\]])[^\S\n]+(?!'.implode('|', $operators).')/', '\\1', $content);
 
         // collapse whitespace around reserved words into single space
@@ -270,9 +294,16 @@ class JS extends Minify
         $content = preg_replace('/(^|[;\}\s])\K('.implode('|', $before).')\s+/', '\\2 ', $content);
         $content = preg_replace('/\s+('.implode('|', $after).')(?=([;\{\s]|$))/', ' \\1', $content);
 
-        // get rid of double semicolons, except when followed by closing-),
-        // where semicolons can be used like: "for(v=1,_=b;;)"
-        $content = preg_replace('/;+(?!\))/', ';', $content);
+        /*
+         * Get rid of double semicolons, except where they can be used like:
+         * "for(v=1,_=b;;)", "for(v=1;;v++)" or "for(;;ja||(ja=true))".
+         * I'll safeguard these double semicolons inside for-loops by
+         * temporarily replacing them with an invalid condition: they won't have
+         * a double semicolon and will be easy to spot to restore afterwards.
+         */
+        $content = preg_replace('/\bfor\(([^;]*);;([^;]*)\)/', 'for(\\1;-;\\2)', $content);
+        $content = preg_replace('/;+/', ';', $content);
+        $content = preg_replace('/\bfor\(([^;]*);-;([^;]*)\)/', 'for(\\1;;\\2)', $content);
 
         /*
          * Next, we'll be removing all semicolons where ASI kicks in.
@@ -286,6 +317,16 @@ class JS extends Minify
          */
         $content = preg_replace('/(for\([^;]*;[^;]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content);
 
+        /*
+         * We also can't strip empty else-statements. Even though they're
+         * useless and probably shouldn't be in the code in the first place, we
+         * shouldn't be stripping the `;` that follows it as it breaks the code.
+         * We can just remove those useless else-statements completely.
+         *
+         * @see https://github.com/matthiasmullie/minify/issues/91
+         */
+        $content = preg_replace('/else;/s', '', $content);
+
         /*
          * We also don't really want to terminate statements followed by closing
          * curly braces (which we've ignored completely up until now) or end-of-
@@ -303,8 +344,9 @@ class JS extends Minify
      * We'll strip whitespace around certain operators with regular expressions.
      * This will prepare the given array by escaping all characters.
      *
-     * @param  string[] $operators
-     * @param  string   $delimiter
+     * @param string[] $operators
+     * @param string   $delimiter
+     *
      * @return string[]
      */
     protected function getOperatorsForRegex(array $operators, $delimiter = '/')
@@ -322,6 +364,10 @@ class JS extends Minify
         // decimal point, or calling a method on it, e.g. 42 .toString()
         $operators['.'] = '(?<![0-9]\s)\.';
 
+        // don't confuse = with other assignment shortcuts (e.g. +=)
+        $chars = preg_quote('+-*\=<>%&|');
+        $operators['='] = '(?<!['.$chars.'])\=';
+
         return $operators;
     }
 
@@ -329,8 +375,9 @@ class JS extends Minify
      * We'll strip whitespace around certain keywords with regular expressions.
      * This will prepare the given array by escaping all characters.
      *
-     * @param  string[] $keywords
-     * @param  string   $delimiter
+     * @param string[] $keywords
+     * @param string   $delimiter
+     *
      * @return string[]
      */
     protected function getKeywordsForRegex(array $keywords, $delimiter = '/')
@@ -350,9 +397,10 @@ class JS extends Minify
     }
 
     /**
-     * Replaces all occurrences of array['key'] by array.key
+     * Replaces all occurrences of array['key'] by array.key.
+     *
+     * @param string $content
      *
-     * @param  string $content
      * @return string
      */
     protected function propertyNotation($content)
@@ -389,7 +437,7 @@ class JS extends Minify
          * we want to use property notation on) - this is to make sure
          * standalone ['value'] arrays aren't confused for keys-of-an-array.
          * We can (and only have to) check the last character, because PHP's
-         * regex implementation doesn't allow un-fixed-length lookbehind
+         * regex implementation doesn't allow unfixed-length look-behind
          * assertions.
          */
         preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar);
@@ -398,18 +446,20 @@ class JS extends Minify
         /*
          * Make sure word preceding the ['value'] is not a keyword, e.g.
          * return['x']. Because -again- PHP's regex implementation doesn't allow
-         * un-fixed-length lookbehind assertions, I'm just going to do a lot of
-         * separate lookbehind assertions, one for each keyword.
+         * unfixed-length look-behind assertions, I'm just going to do a lot of
+         * separate look-behind assertions, one for each keyword.
          */
         $keywords = $this->getKeywordsForRegex($keywords);
         $keywords = '(?<!'.implode(')(?<!', $keywords).')';
+
         return preg_replace_callback('/(?<='.$previousChar.'|\])'.$keywords.'\[(([\'"])[0-9]+\\2)\]/u', $callback, $content);
     }
 
     /**
      * Replaces true & false by !0 and !1.
      *
-     * @param  string $content
+     * @param string $content
+     *
      * @return string
      */
     protected function shortenBools($content)

+ 114 - 65
plugins/matthiasmullie/minify/Minify.php

@@ -1,6 +1,8 @@
 <?php
+
 namespace MatthiasMullie\Minify;
 
+use MatthiasMullie\Minify\Exceptions\IOException;
 use Psr\Cache\CacheItemInterface;
 
 /**
@@ -9,14 +11,13 @@ use Psr\Cache\CacheItemInterface;
  * Please report bugs on https://github.com/matthiasmullie/minify/issues
  *
  * @author Matthias Mullie <minify@mullie.eu>
- *
  * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved.
  * @license MIT License
  */
 abstract class Minify
 {
     /**
-     * The data to be minified
+     * The data to be minified.
      *
      * @var string[]
      */
@@ -76,55 +77,12 @@ abstract class Minify
         }
     }
 
-    /**
-     * Load data.
-     *
-     * @param  string $data Either a path to a file or the content itself.
-     * @return string
-     */
-    protected function load($data)
-    {
-        // check if the data is a file
-        if (@file_exists($data) && is_file($data)) {
-            $data = @file_get_contents($data);
-
-            // strip BOM, if any
-            if (substr($data, 0, 3) == "\xef\xbb\xbf") {
-                $data = substr($data, 3);
-            }
-        }
-
-        return $data;
-    }
-
-    /**
-     * Save to file
-     *
-     * @param  string    $content The minified data.
-     * @param  string    $path    The path to save the minified data to.
-     * @throws Exception
-     */
-    protected function save($content, $path)
-    {
-        // create file & open for writing
-        if (($handler = @fopen($path, 'w')) === false) {
-            throw new Exception('The file "'.$path.'" could not be opened. Check if PHP has enough permissions.');
-        }
-
-        // write to file
-        if (@fwrite($handler, $content) === false) {
-            throw new Exception('The file "'.$path.'" could not be written to. Check if PHP has enough permissions.');
-        }
-
-        // close the file
-        @fclose($handler);
-    }
-
     /**
      * Minify the data & (optionally) saves it to a file.
      *
-     * @param  string[optional] $path Path to write the data to.
-     * @return string           The minified data.
+     * @param string[optional] $path Path to write the data to.
+     *
+     * @return string The minified data.
      */
     public function minify($path = null)
     {
@@ -141,9 +99,10 @@ abstract class Minify
     /**
      * Minify & gzip the data & (optionally) saves it to a file.
      *
-     * @param  string[optional] $path Path to write the data to.
-     * @param  int[optional]    $level Compression level, from 0 to 9.
-     * @return string           The minified & gzipped data.
+     * @param string[optional] $path  Path to write the data to.
+     * @param int[optional]    $level Compression level, from 0 to 9.
+     *
+     * @return string The minified & gzipped data.
      */
     public function gzip($path = null, $level = 9)
     {
@@ -161,8 +120,9 @@ abstract class Minify
     /**
      * Minify the data & write it to a CacheItemInterface object.
      *
-     * @param  CacheItemInterface $item Cache item to write the data to.
-     * @return CacheItemInterface       Cache item with the minifier data.
+     * @param CacheItemInterface $item Cache item to write the data to.
+     *
+     * @return CacheItemInterface Cache item with the minifier data.
      */
     public function cache(CacheItemInterface $item)
     {
@@ -175,17 +135,56 @@ abstract class Minify
     /**
      * Minify the data.
      *
-     * @param  string[optional] $path Path to write the data to.
-     * @return string           The minified data.
+     * @param string[optional] $path Path to write the data to.
+     *
+     * @return string The minified data.
      */
     abstract public function execute($path = null);
 
+    /**
+     * Load data.
+     *
+     * @param string $data Either a path to a file or the content itself.
+     *
+     * @return string
+     */
+    protected function load($data)
+    {
+        // check if the data is a file
+        if ($this->canImportFile($data)) {
+            $data = file_get_contents($data);
+
+            // strip BOM, if any
+            if (substr($data, 0, 3) == "\xef\xbb\xbf") {
+                $data = substr($data, 3);
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Save to file.
+     *
+     * @param string $content The minified data.
+     * @param string $path    The path to save the minified data to.
+     *
+     * @throws IOException
+     */
+    protected function save($content, $path)
+    {
+        $handler = $this->openFileForWriting($path);
+
+        $this->writeToFile($handler, $content);
+
+        @fclose($handler);
+    }
+
     /**
      * Register a pattern to execute against the source content.
      *
-     * @param  string          $pattern     PCRE pattern.
-     * @param  string|callable $replacement Replacement value for matched pattern.
-     * @throws Exception
+     * @param string          $pattern     PCRE pattern.
+     * @param string|callable $replacement Replacement value for matched pattern.
      */
     protected function registerPattern($pattern, $replacement = '')
     {
@@ -203,7 +202,8 @@ abstract class Minify
      * The only way to accurately replace these pieces is to traverse the JS one
      * character at a time and try to find whatever starts first.
      *
-     * @param  string $content The content to replace patterns in.
+     * @param string $content The content to replace patterns in.
+     *
      * @return string The (manipulated) content.
      */
     protected function replace($content)
@@ -284,9 +284,10 @@ abstract class Minify
      * This function will be called plenty of times, where $content will always
      * move up 1 character.
      *
-     * @param  string          $pattern     Pattern to match.
-     * @param  string|callable $replacement Replacement value.
-     * @param  string          $content     Content to match pattern against.
+     * @param string          $pattern     Pattern to match.
+     * @param string|callable $replacement Replacement value.
+     * @param string          $content     Content to match pattern against.
+     *
      * @return string
      */
     protected function replacePattern($pattern, $replacement, $content)
@@ -307,7 +308,7 @@ abstract class Minify
      * placeholder text, so we've rid all strings from characters that may be
      * misinterpreted. Original string content will be saved in $this->extracted
      * and after doing all other minifying, we can restore the original content
-     * via restoreStrings()
+     * via restoreStrings().
      *
      * @param string[optional] $chars
      */
@@ -316,7 +317,8 @@ abstract class Minify
         // PHP only supports $this inside anonymous functions since 5.4
         $minifier = $this;
         $callback = function ($match) use ($minifier) {
-            if (!$match[1]) {
+            // check the second index here, because the first always contains a quote
+            if ($match[2] === '') {
                 /*
                  * Empty strings need no placeholder; they can't be confused for
                  * anything else anyway.
@@ -353,7 +355,8 @@ abstract class Minify
      * replaced with placeholder text in extract*(). The original content was
      * saved in $this->extracted.
      *
-     * @param  string $content
+     * @param string $content
+     *
      * @return string
      */
     protected function restoreExtractedData($content)
@@ -369,4 +372,50 @@ abstract class Minify
 
         return $content;
     }
+
+    /**
+     * Check if the path is a regular file and can be read.
+     *
+     * @param string $path
+     *
+     * @return bool
+     */
+    protected function canImportFile($path)
+    {
+        return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path);
+    }
+
+    /**
+     * Attempts to open file specified by $path for writing.
+     *
+     * @param string $path The path to the file.
+     *
+     * @return resource Specifier for the target file.
+     *
+     * @throws IOException
+     */
+    protected function openFileForWriting($path)
+    {
+        if (($handler = @fopen($path, 'w')) === false) {
+            throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.');
+        }
+
+        return $handler;
+    }
+
+    /**
+     * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions.
+     *
+     * @param resource $handler The resource to write to.
+     * @param string   $content The content to write.
+     * @param string   $path    The path to the file (for exception printing only).
+     *
+     * @throws IOException
+     */
+    protected function writeToFile($handler, $content, $path = '')
+    {
+        if (($result = @fwrite($handler, $content)) === false || ($result < strlen($content))) {
+            throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.');
+        }
+    }
 }

+ 30 - 16
plugins/matthiasmullie/path-converter/Converter.php

@@ -1,4 +1,5 @@
 <?php
+
 namespace MatthiasMullie\PathConverter;
 
 /**
@@ -12,7 +13,6 @@ namespace MatthiasMullie\PathConverter;
  * Please report bugs on https://github.com/matthiasmullie/path-converter/issues
  *
  * @author Matthias Mullie <pathconverter@mullie.eu>
- *
  * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved.
  * @license MIT License
  */
@@ -34,12 +34,26 @@ class Converter
      */
     public function __construct($from, $to)
     {
-        $from = $this->normalize($from);
-        $to = $this->normalize($to);
+        $shared = $this->shared($from, $to);
+        if ($shared === '') {
+            // when both paths have nothing in common, one of them is probably
+            // absolute while the other is relative
+            $cwd = getcwd();
+            $from = strpos($from, $cwd) === 0 ? $from : $cwd.'/'.$from;
+            $to = strpos($to, $cwd) === 0 ? $to : $cwd.'/'.$to;
+
+            // or traveling the tree via `..`
+            // attempt to resolve path, or assume it's fine if it doesn't exist
+            $from = realpath($from) ?: $from;
+            $to = realpath($to) ?: $to;
+        }
 
         $from = $this->dirname($from);
         $to = $this->dirname($to);
 
+        $from = $this->normalize($from);
+        $to = $this->normalize($to);
+
         $this->from = $from;
         $this->to = $to;
     }
@@ -47,14 +61,12 @@ class Converter
     /**
      * Normalize path.
      *
-     * @param  string $path
+     * @param string $path
+     *
      * @return string
      */
     protected function normalize($path)
     {
-        // attempt to resolve path, or assume path is fine if it doesn't exist
-        $path = realpath($path) ?: $path;
-
         // deal with different operating systems' directory structure
         $path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/');
 
@@ -81,8 +93,9 @@ class Converter
      * share
      *     /home/forkcms/frontend
      *
-     * @param  string $path1
-     * @param  string $path2
+     * @param string $path1
+     * @param string $path2
+     *
      * @return string
      */
     protected function shared($path1, $path2)
@@ -116,7 +129,8 @@ class Converter
      *     ../../core/layout/images/img.gif relative to
      *     /home/forkcms/frontend/cache/minified_css
      *
-     * @param  string $path The relative path that needs to be converted.
+     * @param string $path The relative path that needs to be converted.
+     *
      * @return string The new relative path.
      */
     public function convert($path)
@@ -134,12 +148,11 @@ class Converter
 
         // normalize paths
         $path = $this->normalize($this->from.'/'.$path);
-        $to = $this->normalize($this->to);
 
         // strip shared ancestor paths
-        $shared = $this->shared($path, $to);
+        $shared = $this->shared($path, $this->to);
         $path = mb_substr($path, mb_strlen($shared));
-        $to = mb_substr($to, mb_strlen($shared));
+        $to = mb_substr($this->to, mb_strlen($shared));
 
         // add .. for every directory that needs to be traversed to new path
         $to = str_repeat('../', mb_substr_count($to, '/'));
@@ -150,16 +163,17 @@ class Converter
     /**
      * Attempt to get the directory name from a path.
      *
-     * @param  string $path
+     * @param string $path
+     *
      * @return string
      */
     public function dirname($path)
     {
-        if (@is_file($path)) {
+        if (is_file($path)) {
             return dirname($path);
         }
 
-        if (@is_dir($path)) {
+        if (is_dir($path)) {
             return rtrim($path, '/');
         }