Fixes #782 - regression in processing data URIs.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Thu, 2 Jun 2016 06:57:57 +0000 (08:57 +0200)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Thu, 2 Jun 2016 09:16:35 +0000 (11:16 +0200)
Instead of trying to fuzzy match an end of data URI let's use split
to navigate through bracket levels and determine an end of an URI.

History.md
lib/urls/reduce.js
lib/utils/split.js
test/text/urls-processor-test.js
test/utils/split-test.js

index 93f8808..00d0b11 100644 (file)
@@ -7,6 +7,7 @@
 ==================
 
 * Fixed issue [#781](https://github.com/jakubpawlowicz/clean-css/issues/781) - regression in override compacting.
+* Fixed issue [#782](https://github.com/jakubpawlowicz/clean-css/issues/782) - regression in processing data URIs.
 
 [3.4.15 / 2016-06-01](https://github.com/jakubpawlowicz/clean-css/compare/v3.4.14...v3.4.15)
 ==================
index 8dcbab6..783277e 100644 (file)
@@ -1,9 +1,11 @@
+var split = require('../utils/split');
+
 var URL_PREFIX = 'url(';
 var UPPERCASE_URL_PREFIX = 'URL(';
 var URL_SUFFIX = ')';
 
 var DATA_URI_PREFIX_PATTERN = /^\s*['"]?\s*data:/;
-var DATA_URI_END_FUZZY_PATTERN = /^\)[\s\{\};]/;
+var DATA_URI_TRAILER_PATTERN = /[\s\};,\/]/;
 
 var IMPORT_URL_PREFIX = '@import';
 var UPPERCASE_IMPORT_URL_PREFIX = '@IMPORT';
@@ -14,7 +16,7 @@ function byUrl(data, context, callback) {
   var nextStart = 0;
   var nextStartUpperCase = 0;
   var nextEnd = 0;
-  var nextEndAhead = 0;
+  var firstMatch;
   var isDataURI = false;
   var cursor = 0;
   var tempData = [];
@@ -32,18 +34,12 @@ function byUrl(data, context, callback) {
     isDataURI = DATA_URI_PREFIX_PATTERN.test(data.substring(nextStart + URL_PREFIX.length));
 
     if (isDataURI) {
-      nextEnd = data.indexOf(URL_SUFFIX, nextStart);
-
-      // this is a fuzzy matching logic for unqoted data URIs
-      while (true) {
-        nextEndAhead = data.indexOf(URL_SUFFIX, nextEnd + 1);
-        // if it has whitespace, curly braces, or semicolon then we should be out of URL,
-        // otherwise keep iterating if it has not but content is not escaped,
-        // it has to be quoted so it will be captured by either of two clauses above
-        if (nextEndAhead == -1 || DATA_URI_END_FUZZY_PATTERN.test(data.substring(nextEnd, nextEndAhead)))
-          break;
-
-        nextEnd = nextEndAhead;
+      firstMatch = split(data.substring(nextStart), DATA_URI_TRAILER_PATTERN, false, '(', ')', true).pop();
+
+      if (firstMatch && firstMatch[firstMatch.length - 1] == URL_SUFFIX) {
+        nextEnd = nextStart + firstMatch.length - URL_SUFFIX.length;
+      } else {
+        nextEnd = -1;
       }
     } else {
       if (data[nextStart + URL_PREFIX.length] == '"') {
index 178c739..899ce29 100644 (file)
@@ -1,4 +1,4 @@
-function split(value, separator, includeSeparator, openLevel, closeLevel) {
+function split(value, separator, includeSeparator, openLevel, closeLevel, firstOnly) {
   var withRegex = typeof separator != 'string';
   var hasSeparator = withRegex ?
     separator.test(value) :
@@ -10,7 +10,7 @@ function split(value, separator, includeSeparator, openLevel, closeLevel) {
   openLevel = openLevel || '(';
   closeLevel = closeLevel || ')';
 
-  if (value.indexOf(openLevel) == -1 && !includeSeparator)
+  if (value.indexOf(openLevel) == -1 && !includeSeparator && !firstOnly)
     return value.split(separator);
 
   var level = 0;
@@ -29,6 +29,10 @@ function split(value, separator, includeSeparator, openLevel, closeLevel) {
     if (level === 0 && cursor > 0 && cursor + 1 < len && (withRegex ? separator.test(value[cursor]) : value[cursor] == separator)) {
       tokens.push(value.substring(lastStart, cursor + (includeSeparator ? 1 : 0)));
       lastStart = cursor + 1;
+
+      if (firstOnly && tokens.length == 1) {
+        break;
+      }
     }
 
     cursor++;
index 04974ed..54111df 100644 (file)
@@ -86,15 +86,20 @@ vows.describe(UrlsProcessor)
         'a{background:__ESCAPED_URL_CLEAN_CSS0__}div:not([test]){color:red}',
         'a{background:url(url)}div:not([test]){color:red}'
       ],
-      'data URI with single brackets': [
+      'data URI wrapped in single quotes': [
         'a{background-image:url(\'data:image/svg+xml;charset=utf-8,<svg viewBox=\'0 0 120 120\'><g transform=\'rotate(30 60,60)\'></g></svg>\')}',
         'a{background-image:__ESCAPED_URL_CLEAN_CSS0__}',
         'a{background-image:url(\'data:image/svg+xml;charset=utf-8,<svg viewBox=\'0 0 120 120\'><g transform=\'rotate(30 60,60)\'></g></svg>\')}'
       ],
-      'data URI with double brackets': [
+      'data URI wrapped in double quotes': [
         'a{background-image:url("data:image/svg+xml;charset=utf-8,<svg viewBox=\'0 0 120 120\'><g transform=\'rotate(30 60,60)\'></g></svg>")}',
         'a{background-image:__ESCAPED_URL_CLEAN_CSS0__}',
         'a{background-image:url("data:image/svg+xml;charset=utf-8,<svg viewBox=\'0 0 120 120\'><g transform=\'rotate(30 60,60)\'></g></svg>")}'
+      ],
+      'two quoted data URIs with closing brackets': [
+        '.a{cursor:url("data:application/octet-stream;base64,A...rotate(30 60,60)...="),move!important}.b{cursor:url("data:application/octet-stream;base64,A...rotate(30 60,60)...=")}',
+        '.a{cursor:__ESCAPED_URL_CLEAN_CSS0__,move!important}.b{cursor:__ESCAPED_URL_CLEAN_CSS0__}',
+        '.a{cursor:url("data:application/octet-stream;base64,A...rotate(30 60,60)...="),move!important}.b{cursor:url("data:application/octet-stream;base64,A...rotate(30 60,60)...=")}',
       ]
     })
   )
index 9357091..ab0835e 100644 (file)
@@ -129,4 +129,24 @@ vows.describe(split)
       }
     }
   })
+  .addBatch({
+    'just first one': {
+      topic: 'linear-gradient(0, #fff, rgba(0, 0, 0)) red',
+      split: function (input) {
+        assert.deepEqual(split(input, ' ', false, '(', ')', true), ['linear-gradient(0, #fff, rgba(0, 0, 0))']);
+      }
+    },
+    'just first one when no opening token': {
+      topic: 'red blue',
+      split: function (input) {
+        assert.deepEqual(split(input, ' ', false, '(', ')', true), ['red']);
+      }
+    },
+    'just first one when no closing token in last token': {
+      topic: 'red linear-gradient(0 0',
+      split: function (input) {
+        assert.deepEqual(split(input, ' ', false, '(', ')', true), ['red']);
+      }
+    }
+  })
   .export(module);