Improves CommentsProcessor.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Thu, 18 Sep 2014 07:51:06 +0000 (09:51 +0200)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Fri, 10 Oct 2014 20:22:43 +0000 (21:22 +0100)
* Adds prototypal OO.
* Adds basic tests.

lib/clean.js
lib/text/comments-processor.js [new file with mode: 0644]
lib/text/comments.js [deleted file]
test/text/comments-processor-test.js [new file with mode: 0644]

index 66345cb..55d4dd4 100644 (file)
@@ -41,7 +41,6 @@ var CleanCSS = module.exports = function CleanCSS(options) {
   };
   this.errors = this.context.errors;
   this.warnings = this.context.warnings;
-  this.lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
 };
 
 CleanCSS.prototype.minify = function(data, callback) {
@@ -77,13 +76,8 @@ var minify = function(data, callback) {
   var stats = this.stats;
   var options = this.options;
   var context = this.context;
-  var lineBreak = this.lineBreak;
 
-  var commentsProcessor = new CommentsProcessor(
-    'keepSpecialComments' in options ? options.keepSpecialComments : '*',
-    options.keepBreaks,
-    lineBreak
-  );
+  var commentsProcessor = new CommentsProcessor('keepSpecialComments' in options ? options.keepSpecialComments : '*', options.keepBreaks);
   var expressionsProcessor = new ExpressionsProcessor();
   var freeTextProcessor = new FreeTextProcessor();
   var urlsProcessor = new UrlsProcessor();
diff --git a/lib/text/comments-processor.js b/lib/text/comments-processor.js
new file mode 100644 (file)
index 0000000..800adbb
--- /dev/null
@@ -0,0 +1,101 @@
+var EscapeStore = require('./escape-store');
+var QuoteScanner = require('./quote-scanner');
+
+var SPECIAL_COMMENT_PREFIX = '/*!';
+var COMMENT_PREFIX = '/*';
+var COMMENT_SUFFIX = '*/';
+
+var lineBreak = require('os').EOL;
+
+var CommentsProcessor = function CommentsProcessor(keepSpecialComments, keepBreaks) {
+  this.comments = new EscapeStore('COMMENT');
+  this.keepAll = keepSpecialComments == '*';
+  this.keepOne = keepSpecialComments == '1' || keepSpecialComments === 1;
+  this.keepBreaks = keepBreaks;
+};
+
+function quoteScannerFor(data) {
+  var quoteMap = [];
+  new QuoteScanner(data).each(function (quotedString, _, startsAt) {
+    quoteMap.push([startsAt, startsAt + quotedString.length]);
+  });
+
+  return function (position) {
+    for (var i = 0, l = quoteMap.length; i < l; i++) {
+      if (quoteMap[i][0] < position && quoteMap[i][1] > position)
+        return true;
+    }
+
+    return false;
+  };
+}
+
+CommentsProcessor.prototype.escape = function (data) {
+  var tempData = [];
+  var nextStart = 0;
+  var nextEnd = 0;
+  var cursor = 0;
+  var isQuotedAt = quoteScannerFor(data);
+
+  for (; nextEnd < data.length;) {
+    nextStart = data.indexOf(COMMENT_PREFIX, cursor);
+    if (nextStart == -1)
+      break;
+
+    if (isQuotedAt(nextStart)) {
+      tempData.push(data.substring(cursor, nextStart + COMMENT_PREFIX.length));
+      cursor = nextStart + COMMENT_PREFIX.length;
+      continue;
+    }
+
+    nextEnd = data.indexOf(COMMENT_SUFFIX, nextStart + COMMENT_PREFIX.length);
+    if (nextEnd == -1)
+      break;
+
+    tempData.push(data.substring(cursor, nextStart));
+
+    var comment = data.substring(nextStart, nextEnd + COMMENT_SUFFIX.length);
+    if (comment.indexOf(SPECIAL_COMMENT_PREFIX) === 0) {
+      var placeholder = this.comments.store(comment);
+      tempData.push(placeholder);
+    }
+
+    cursor = nextEnd + COMMENT_SUFFIX.length;
+  }
+
+  return tempData.length > 0 ?
+    tempData.join('') + data.substring(cursor, data.length) :
+    data;
+};
+
+CommentsProcessor.prototype.restore = function (data) {
+  var tempData = [];
+  var restored = 0;
+  var cursor = 0;
+  var addBreak;
+
+  for (; cursor < data.length;) {
+    var nextMatch = this.comments.nextMatch(data, cursor);
+    if (nextMatch.start < 0)
+      break;
+
+    tempData.push(data.substring(cursor, nextMatch.start));
+    var comment = this.comments.restore(nextMatch.match);
+
+    if (comment.indexOf(SPECIAL_COMMENT_PREFIX) === 0 && (this.keepAll || (this.keepOne && restored === 0))) {
+      restored++;
+      addBreak = this.keepBreaks && data[nextMatch.end] != '\n' && data[nextMatch.end] != '\r\n';
+      tempData.push(comment, addBreak ? lineBreak : '');
+    } else {
+      nextMatch.end += this.keepBreaks ? 1 : 0;
+    }
+
+    cursor = nextMatch.end;
+  }
+
+  return tempData.length > 0 ?
+    tempData.join('') + data.substring(cursor, data.length) :
+    data;
+};
+
+module.exports = CommentsProcessor;
diff --git a/lib/text/comments.js b/lib/text/comments.js
deleted file mode 100644 (file)
index c715100..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-var EscapeStore = require('./escape-store');
-var QuoteScanner = require('./quote-scanner');
-
-module.exports = function Comments(keepSpecialComments, keepBreaks, lineBreak) {
-  var comments = new EscapeStore('COMMENT');
-
-  return {
-    // Strip special comments (/*! ... */) by replacing them by a special marker
-    // for further restoring. Plain comments are removed. It's done by scanning data using
-    // String#indexOf scanning instead of regexps to speed up the process.
-    escape: function(data) {
-      var tempData = [];
-      var nextStart = 0;
-      var nextEnd = 0;
-      var cursor = 0;
-      var isQuotedAt = (function () {
-        var quoteMap = [];
-        new QuoteScanner(data).each(function (quotedString, _, startsAt) {
-          quoteMap.push([startsAt, startsAt + quotedString.length]);
-        });
-
-        return function (position) {
-          for (var i = 0, l = quoteMap.length; i < l; i++) {
-            if (quoteMap[i][0] < position && quoteMap[i][1] > position)
-              return true;
-          }
-
-          return false;
-        };
-      })();
-
-      for (; nextEnd < data.length;) {
-        nextStart = data.indexOf('/*', cursor);
-        if (nextStart == -1)
-          break;
-        if (isQuotedAt(nextStart)) {
-          tempData.push(data.substring(cursor, nextStart + 2));
-          cursor = nextStart + 2;
-          continue;
-        }
-
-        nextEnd = data.indexOf('*/', nextStart + 2);
-        if (nextEnd == -1)
-          break;
-
-        tempData.push(data.substring(cursor, nextStart));
-        if (data[nextStart + 2] == '!') {
-          // in case of special comments, replace them with a placeholder
-          var comment = data.substring(nextStart, nextEnd + 2);
-          var placeholder = comments.store(comment);
-          tempData.push(placeholder);
-        }
-        cursor = nextEnd + 2;
-      }
-
-      return tempData.length > 0 ?
-        tempData.join('') + data.substring(cursor, data.length) :
-        data;
-    },
-
-    restore: function(data) {
-      var restored = 0;
-      var breakSuffix = keepBreaks ? lineBreak : '';
-
-      return data.replace(new RegExp(comments.placeholderPattern + '(' + lineBreak + '| )?', 'g'), function(match, placeholder) {
-        restored++;
-
-        switch (keepSpecialComments) {
-          case '*':
-            return comments.restore(placeholder) + breakSuffix;
-          case 1:
-          case '1':
-            return restored == 1 ?
-              comments.restore(placeholder) + breakSuffix :
-              '';
-          case 0:
-          case '0':
-            return '';
-        }
-      });
-    }
-  };
-};
diff --git a/test/text/comments-processor-test.js b/test/text/comments-processor-test.js
new file mode 100644 (file)
index 0000000..92e6348
--- /dev/null
@@ -0,0 +1,142 @@
+var vows = require('vows');
+var assert = require('assert');
+var CommentsProcessor = require('../../lib/text/comments-processor');
+
+var lineBreak = require('os').EOL;
+
+function processorContext(name, context, keepSpecialComments, keepBreaks) {
+  var vowContext = {};
+
+  function escaped (targetCSS) {
+    return function (sourceCSS) {
+      var result = new CommentsProcessor(keepSpecialComments, keepBreaks).escape(sourceCSS);
+      assert.equal(result, targetCSS);
+    };
+  }
+
+  function restored (targetCSS) {
+    return function (sourceCSS) {
+      var processor = new CommentsProcessor(keepSpecialComments, keepBreaks);
+      var result = processor.restore(processor.escape(sourceCSS));
+      assert.equal(result, targetCSS);
+    };
+  }
+
+  for (var key in context) {
+    vowContext[name + ' - ' + key] = {
+      topic: context[key][0],
+      escaped: escaped(context[key][1]),
+      restored: restored(context[key][2])
+    };
+  }
+
+  return vowContext;
+}
+
+vows.describe(CommentsProcessor)
+  .addBatch(
+    processorContext('all', {
+      'no comments': [
+        'a{color:red}',
+        'a{color:red}',
+        'a{color:red}'
+      ],
+      'one comment': [
+        '/* some text */',
+        '',
+        ''
+      ],
+      'one special comment': [
+        '/*! some text */',
+        '__ESCAPED_COMMENT_CLEAN_CSS0__',
+        '/*! some text */'
+      ],
+      'two comments': [
+        '/* one text *//* another text */',
+        '',
+        ''
+      ],
+      'two same comments': [
+        '/* one text *//* one text */',
+        '',
+        ''
+      ],
+      'two special comments': [
+        '/*! one text *//*! another text */',
+        '__ESCAPED_COMMENT_CLEAN_CSS0____ESCAPED_COMMENT_CLEAN_CSS1__',
+        '/*! one text *//*! another text */'
+      ],
+      'commented selector': [
+        '/* a{color:red} */',
+        '',
+        ''
+      ],
+      'quoted comment': [
+        'a{content:"/* text */"}',
+        'a{content:"/* text */"}',
+        'a{content:"/* text */"}'
+      ]
+    }, '*')
+  )
+  .addBatch(
+    processorContext('one', {
+      'one comment': [
+        '/* some text */',
+        '',
+        ''
+      ],
+      'one special comment': [
+        '/*! some text */',
+        '__ESCAPED_COMMENT_CLEAN_CSS0__',
+        '/*! some text */'
+      ],
+      'two special comments': [
+        '/*! one text *//*! another text */',
+        '__ESCAPED_COMMENT_CLEAN_CSS0____ESCAPED_COMMENT_CLEAN_CSS1__',
+        '/*! one text */'
+      ]
+    }, '1')
+  )
+  .addBatch(
+    processorContext('zero', {
+      'one comment': [
+        '/* some text */',
+        '',
+        ''
+      ],
+      'one special comment': [
+        '/*! some text */',
+        '__ESCAPED_COMMENT_CLEAN_CSS0__',
+        ''
+      ],
+      'two special comments': [
+        '/*! one text *//*! another text */',
+        '__ESCAPED_COMMENT_CLEAN_CSS0____ESCAPED_COMMENT_CLEAN_CSS1__',
+        ''
+      ]
+    }, '0')
+  )
+  .addBatch(
+    processorContext('zero with breaks', {
+      'content and special comments': [
+        'a{}' + lineBreak + '/*! some text */' + lineBreak + 'p{}',
+        'a{}' + lineBreak + '__ESCAPED_COMMENT_CLEAN_CSS0__' + lineBreak + 'p{}',
+        'a{}' + lineBreak + 'p{}'
+      ]
+    }, '0', true)
+  )
+  .addBatch(
+    processorContext('one with breaks', {
+      'forces break after comments': [
+        'a{}/*! some text */p{}',
+        'a{}__ESCAPED_COMMENT_CLEAN_CSS0__p{}',
+        'a{}/*! some text */' + lineBreak + 'p{}'
+      ],
+      'if not given already comments': [
+        'a{}/*! some text */' + lineBreak + 'p{}',
+        'a{}__ESCAPED_COMMENT_CLEAN_CSS0__' + lineBreak + 'p{}',
+        'a{}/*! some text */' + lineBreak + 'p{}'
+      ]
+    }, '1', true)
+  )
+  .export(module);