[BUGFIX] CSS3 function calc must retain whitespace 80/34180/3
authorChristian Futterlieb <christian@futterlieb.ch>
Fri, 14 Nov 2014 18:17:10 +0000 (19:17 +0100)
committerMarkus Klein <klein.t3@reelworx.at>
Sun, 18 Jan 2015 13:27:05 +0000 (14:27 +0100)
Whitespaces within CSS3 function 'calc' must not be stripped
because otherwise browsers won't recognize it anymore.

Although the CSS3 specification requires the whitespaces around
the additive expressions (+/-) only, it might be better to not
remove any whitespace within a calc function in terms of
simplicity.. the minifying is complex enough as it already is.

Resolves: #62463
Releases: master, 6.2
Change-Id: Ied0c02e132aafa97ce9fb6b0e9930898cb17efc1
Reviewed-on: http://review.typo3.org/34180
Reviewed-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Tested-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Reviewed-by: Markus Klein <klein.t3@reelworx.at>
Tested-by: Markus Klein <klein.t3@reelworx.at>
typo3/sysext/core/Classes/Resource/ResourceCompressor.php
typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorTest.php

index 05ac6e8..5b785be 100644 (file)
@@ -385,38 +385,8 @@ class ResourceCompressor {
                // only create it, if it doesn't exist, yet
                if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
                        $contents = GeneralUtility::getUrl($filenameAbsolute);
-                       // Perform some safe CSS optimizations.
-                       $contents = str_replace(CR, '', $contents);
-                       // Strip any and all carriage returns.
-                       // Match and process strings, comments and everything else, one chunk at a time.
-                       // To understand this regex, read: "Mastering Regular Expressions 3rd Edition" chapter 6.
-                       $contents = preg_replace_callback('%
-                               # One-regex-to-rule-them-all! - version: 20100220_0100
-                               # Group 1: Match a double quoted string.
-                               ("[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+") |  # or...
-                               # Group 2: Match a single quoted string.
-                               (\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\') |  # or...
-                               # Group 3: Match a regular non-MacIE5-hack comment.
-                               (/\\*[^\\\\*]*+\\*++(?:[^\\\\*/][^\\\\*]*+\\*++)*+/) |  # or...
-                               # Group 4: Match a MacIE5-type1 comment.
-                               (/\\*(?:[^*\\\\]*+\\**+(?!/))*+\\\\[^*]*+\\*++(?:[^*/][^*]*+\\*++)*+/(?<!\\\\\\*/)) |  # or...
-                               # Group 5: Match a MacIE5-type2 comment.
-                               (/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/(?<=\\\\\\*/))  # folllowed by...
-                               # Group 6: Match everything up to final closing regular comment
-                               ([^/]*+(?:(?!\\*)/[^/]*+)*?)
-                               # Group 7: Match final closing regular comment
-                               (/\\*[^/]++(?:(?<!\\*)/(?!\\*)[^/]*+)*+/(?<=(?<!\\\\)\\*/)) |  # or...
-                               # Group 8: Match regular non-string, non-comment text.
-                               ([^"\'/]*+(?:(?!/\\*)/[^"\'/]*+)*+)
-                               %Ssx', array('self', 'compressCssPregCallback'), $contents);
-                       // Do it!
-                       $contents = preg_replace('/^\\s++/', '', $contents);
-                       // Strip leading whitespace.
-                       $contents = preg_replace('/[ \\t]*+\\n\\s*+/S', '
-', $contents);
-                       // Consolidate multi-lines space.
-                       $contents = preg_replace('/(?<!\\s)\\s*+$/S', '
-', $contents);
+                       // Compress CSS Content
+                       $contents = $this->compressCssString($contents);
                        // Ensure file ends in newline.
                        // we have to fix relative paths, if we aren't working on a file in our target directory
                        if (strpos($filename, $this->targetDirectory) === FALSE) {
@@ -464,22 +434,25 @@ class ResourceCompressor {
 /*T2\\*/' . $matches[6] . '
 /*T2E*/
 ';
-               } elseif (isset($matches[8])) {
-                       // Group 8: Non-string, non-comment. Safe to clean whitespace here.
-                       $matches[8] = preg_replace('/^\\s++/', '', $matches[8]);
+               } elseif ($matches[8]) {
+                       // Group 8: calc function (see http://www.w3.org/TR/2006/WD-css3-values-20060919/#calc)
+                       return 'calc' . $matches[8];
+               } elseif (isset($matches[9])) {
+                       // Group 9: Non-string, non-comment. Safe to clean whitespace here.
+                       $matches[9] = preg_replace('/^\\s++/', '', $matches[9]);
                        // Strip all leading whitespace.
-                       $matches[8] = preg_replace('/\\s++$/', '', $matches[8]);
+                       $matches[9] = preg_replace('/\\s++$/', '', $matches[9]);
                        // Strip all trailing whitespace.
-                       $matches[8] = preg_replace('/\\s{2,}+/', ' ', $matches[8]);
+                       $matches[9] = preg_replace('/\\s{2,}+/', ' ', $matches[9]);
                        // Consolidate multiple whitespace.
-                       $matches[8] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[8]);
+                       $matches[9] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[9]);
                        // Clean pre-punctuation.
-                       $matches[8] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[8]);
+                       $matches[9] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[9]);
                        // Clean post-punctuation.
-                       $matches[8] = preg_replace('/;?\\}/S', '}
-', $matches[8]);
+                       $matches[9] = preg_replace('/;?\\}/S', '}
+', $matches[9]);
                        // Add a touch of formatting.
-                       return $matches[8];
+                       return $matches[9];
                }
                return $matches[0] . '
 /* ERROR! Unexpected _proccess_css_minify() parameter */
@@ -717,4 +690,49 @@ class ResourceCompressor {
                return $filename;
        }
 
+       /**
+        * Compress a CSS string by removing comments and whitespace characters
+        *
+        * @param string $contents
+        * @return string
+        */
+       protected function compressCssString($contents) {
+               // Perform some safe CSS optimizations.
+               $contents = str_replace(CR, '', $contents);
+               // Strip any and all carriage returns.
+               // Match and process strings, comments and everything else, one chunk at a time.
+               // To understand this regex, read: "Mastering Regular Expressions 3rd Edition" chapter 6.
+               $contents = preg_replace_callback('%
+                               # One-regex-to-rule-them-all! - version: 20100220_0100
+                               # Group 1: Match a double quoted string.
+                               ("[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+") |  # or...
+                               # Group 2: Match a single quoted string.
+                               (\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\') |  # or...
+                               # Group 3: Match a regular non-MacIE5-hack comment.
+                               (/\\*[^\\\\*]*+\\*++(?:[^\\\\*/][^\\\\*]*+\\*++)*+/) |  # or...
+                               # Group 4: Match a MacIE5-type1 comment.
+                               (/\\*(?:[^*\\\\]*+\\**+(?!/))*+\\\\[^*]*+\\*++(?:[^*/][^*]*+\\*++)*+/(?<!\\\\\\*/)) |  # or...
+                               # Group 5: Match a MacIE5-type2 comment.
+                               (/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/(?<=\\\\\\*/))  # folllowed by...
+                               # Group 6: Match everything up to final closing regular comment
+                               ([^/]*+(?:(?!\\*)/[^/]*+)*?)
+                               # Group 7: Match final closing regular comment
+                               (/\\*[^/]++(?:(?<!\\*)/(?!\\*)[^/]*+)*+/(?<=(?<!\\\\)\\*/)) |  # or...
+                               # Group 8: Match a calc function (see http://www.w3.org/TR/2006/WD-css3-values-20060919/#calc)
+                               (?:calc(\\((?:(?:[^\\(\\)]+)|(?8))*+\\))) | # or...
+                               # Group 9: Match regular non-string, non-comment text.
+                               ((?:[^"\'/](?!calc))*+(?:(?!/\\*)/(?:[^"\'/](?!calc))*+)*+)
+                               %Ssx', array('self', 'compressCssPregCallback'), $contents);
+               // Do it!
+               $contents = preg_replace('/^\\s++/', '', $contents);
+               // Strip leading whitespace.
+               $contents = preg_replace('/[ \\t]*+\\n\\s*+/S', '
+', $contents);
+               // Consolidate multi-lines space.
+               $contents = preg_replace('/(?<!\\s)\\s*+$/S', '
+', $contents);
+
+               return $contents;
+       }
+
 }
index c957d7a..57bcc7c 100644 (file)
@@ -177,4 +177,37 @@ class ResourceCompressorTest extends BaseTestCase {
                $this->assertTrue($result[$concatenatedFileName]['excludeFromConcatenation']);
        }
 
-}
\ No newline at end of file
+       /**
+        * @return array
+        */
+       public function calcStatementsDataProvider() {
+               return array(
+                       'simple calc' => array(
+                               'calc(100% - 3px)',
+                               'calc(100% - 3px)',
+                       ),
+                       'complex calc with parentheses at the beginning' => array(
+                               'calc((100%/20) - 2*3px)',
+                               'calc((100%/20) - 2*3px)',
+                       ),
+                       'complex calc with parentheses at the end' => array(
+                               'calc(100%/20 - 2*3px - (200px + 3%))',
+                               'calc(100%/20 - 2*3px - (200px + 3%))',
+                       ),
+                       'complex calc with many parentheses' => array(
+                               'calc((100%/20) - (2 * (3px - (200px + 3%))))',
+                               'calc((100%/20) - (2 * (3px - (200px + 3%))))',
+                       ),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider calcStatementsDataProvider
+        */
+       public function calcFunctionMustRetainWhitespaces($input, $expected) {
+               $result = $this->subject->_call('compressCssString', $input);
+               $this->assertSame($expected, trim($result));
+    }
+
+}