From: Jakub Pawlowicz Date: Thu, 18 Sep 2014 07:51:06 +0000 (+0200) Subject: Improves CommentsProcessor. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=d855b91e853c0e369b807fde569918bc7b7b5a54;p=clean-css.git Improves CommentsProcessor. * Adds prototypal OO. * Adds basic tests. --- diff --git a/lib/clean.js b/lib/clean.js index 66345cbd..55d4dd47 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -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 index 00000000..800adbbf --- /dev/null +++ b/lib/text/comments-processor.js @@ -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 index c7151002..00000000 --- a/lib/text/comments.js +++ /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 index 00000000..92e6348c --- /dev/null +++ b/test/text/comments-processor-test.js @@ -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);