From: Jakub Pawlowicz Date: Wed, 24 Sep 2014 19:56:25 +0000 (+0100) Subject: Improves ExpressionsProcessor. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=2b784ca98d37cf9d56acd551335649c5cf242a00;p=clean-css.git Improves ExpressionsProcessor. * Adds prototypal declarations. * Adds tests. --- diff --git a/lib/text/expressions-processor.js b/lib/text/expressions-processor.js new file mode 100644 index 00000000..02e72ead --- /dev/null +++ b/lib/text/expressions-processor.js @@ -0,0 +1,98 @@ +var EscapeStore = require('./escape-store'); + +var EXPRESSION_NAME = 'expression'; +var EXPRESSION_START = '('; +var EXPRESSION_END = ')'; +var EXPRESSION_PREFIX = EXPRESSION_NAME + EXPRESSION_START; +var BODY_START = '{'; +var BODY_END = '}'; + +function findEnd(data, start) { + var end = start + EXPRESSION_NAME.length; + var level = 0; + var quoted = false; + var braced = false; + + while (true) { + var current = data[end++]; + + if (quoted) { + quoted = current != '\'' && current != '"'; + } else { + quoted = current == '\'' || current == '"'; + + if (current == EXPRESSION_START) + level++; + if (current == EXPRESSION_END) + level--; + if (current == BODY_START) + braced = true; + if (current == BODY_END && !braced && level == 1) { + end--; + level--; + } + } + + if (level === 0 && current == EXPRESSION_END) + break; + if (!current) { + end = data.substring(0, end).lastIndexOf(BODY_END); + break; + } + } + + return end; +} + +var ExpressionsProcessor = function ExpressionsProcessor() { + this.expressions = new EscapeStore('EXPRESSION'); +}; + +ExpressionsProcessor.prototype.escape = function (data) { + var nextStart = 0; + var nextEnd = 0; + var cursor = 0; + var tempData = []; + + for (; nextEnd < data.length;) { + nextStart = data.indexOf(EXPRESSION_PREFIX, nextEnd); + if (nextStart == -1) + break; + + nextEnd = findEnd(data, nextStart); + + var expression = data.substring(nextStart, nextEnd); + var placeholder = this.expressions.store(expression); + tempData.push(data.substring(cursor, nextStart)); + tempData.push(placeholder); + + cursor = nextEnd; + } + + return tempData.length > 0 ? + tempData.join('') + data.substring(cursor, data.length) : + data; +}; + +ExpressionsProcessor.prototype.restore = function (data) { + var tempData = []; + var cursor = 0; + + for (; cursor < data.length;) { + var nextMatch = this.expressions.nextMatch(data, cursor); + if (nextMatch.start < 0) + break; + + tempData.push(data.substring(cursor, nextMatch.start)); + var comment = this.expressions.restore(nextMatch.match); + tempData.push(comment); + + cursor = nextMatch.end; + } + + return tempData.length > 0 ? + tempData.join('') + data.substring(cursor, data.length) : + data; +}; + +module.exports = ExpressionsProcessor; diff --git a/lib/text/expressions.js b/lib/text/expressions.js deleted file mode 100644 index b231d85f..00000000 --- a/lib/text/expressions.js +++ /dev/null @@ -1,73 +0,0 @@ -var EscapeStore = require('./escape-store'); - -module.exports = function Expressions() { - var expressions = new EscapeStore('EXPRESSION'); - - var findEnd = function(data, start) { - var end = start + 'expression'.length; - var level = 0; - var quoted = false; - - while (true) { - var next = data[end++]; - - if (quoted) { - quoted = next != '\'' && next != '"'; - } else { - quoted = next == '\'' || next == '"'; - - if (next == '(') - level++; - if (next == ')') - level--; - if (next == '}' && level == 1) { - end--; - level--; - } - } - - if (level === 0 && next == ')') - break; - if (!next) { - end = data.substring(0, end).lastIndexOf('}'); - break; - } - } - - return end; - }; - - return { - // Escapes expressions by replacing them by a special - // marker for further restoring. It's done via string scanning - // instead of regexps to speed up the process. - escape: function(data) { - var nextStart = 0; - var nextEnd = 0; - var cursor = 0; - var tempData = []; - - for (; nextEnd < data.length;) { - nextStart = data.indexOf('expression(', nextEnd); - if (nextStart == -1) - break; - - nextEnd = findEnd(data, nextStart); - - var expression = data.substring(nextStart, nextEnd); - var placeholder = expressions.store(expression); - tempData.push(data.substring(cursor, nextStart)); - tempData.push(placeholder); - cursor = nextEnd; - } - - return tempData.length > 0 ? - tempData.join('') + data.substring(cursor, data.length) : - data; - }, - - restore: function(data) { - return data.replace(expressions.placeholderRegExp, expressions.restore); - } - }; -}; diff --git a/test/text/expressions-processor-test.js b/test/text/expressions-processor-test.js new file mode 100644 index 00000000..798de3b3 --- /dev/null +++ b/test/text/expressions-processor-test.js @@ -0,0 +1,84 @@ +var vows = require('vows'); +var assert = require('assert'); +var ExpressionsProcessor = require('../../lib/text/expressions-processor'); + +function processorContext(context) { + var vowContext = {}; + + function escaped (targetCSS) { + return function (sourceCSS) { + var result = new ExpressionsProcessor().escape(sourceCSS); + assert.equal(result, targetCSS); + }; + } + + function restored (targetCSS) { + return function (sourceCSS) { + var processor = new ExpressionsProcessor(); + var result = processor.restore(processor.escape(sourceCSS)); + assert.equal(result, targetCSS); + }; + } + + for (var key in context) { + vowContext[key] = { + topic: context[key][0], + escaped: escaped(context[key][1]), + restored: restored(context[key][2]) + }; + } + + return vowContext; +} + +vows.describe(ExpressionsProcessor) + .addBatch( + processorContext({ + 'empty': [ + 'a{color:expression()}', + 'a{color:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{color:expression()}' + ], + 'method call': [ + 'a{color:expression(this.parentNode.currentStyle.color)}', + 'a{color:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{color:expression(this.parentNode.currentStyle.color)}' + ], + 'multiple calls': [ + 'a{color:expression(x = 0 , this.parentNode.currentStyle.color)}', + 'a{color:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{color:expression(x = 0 , this.parentNode.currentStyle.color)}' + ], + 'mixed content': [ + 'a{zoom:expression(this.runtimeStyle[\"zoom\"] = \'1\', this.innerHTML = \'\')}', + 'a{zoom:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{zoom:expression(this.runtimeStyle[\"zoom\"] = \'1\', this.innerHTML = \'\')}' + ], + 'complex': [ + 'a{width:expression((this.parentNode.innerWidth + this.parentNode.innerHeight) / 2 )}', + 'a{width:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{width:expression((this.parentNode.innerWidth + this.parentNode.innerHeight) / 2 )}' + ], + 'with parentheses': [ + 'a{width:expression(this.parentNode.innerText == \')\' ? \'5px\' : \'10px\' )}', + 'a{width:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{width:expression(this.parentNode.innerText == \')\' ? \'5px\' : \'10px\' )}' + ], + 'open ended (broken)': [ + 'a{width:expression(this.parentNode.innerText == }', + 'a{width:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{width:expression(this.parentNode.innerText == }' + ], + 'function call & advanced': [ + 'a{zoom:expression(function(el){el.style.zoom="1"}(this))}', + 'a{zoom:__ESCAPED_EXPRESSION_CLEAN_CSS0__}', + 'a{zoom:expression(function(el){el.style.zoom="1"}(this))}' + ], + 'with more properties': [ + 'a{color:red;zoom:expression(function(el){el.style.zoom="1"}(this));display:block}', + 'a{color:red;zoom:__ESCAPED_EXPRESSION_CLEAN_CSS0__;display:block}', + 'a{color:red;zoom:expression(function(el){el.style.zoom="1"}(this));display:block}' + ] + }) + ) + .export(module);