* Adds prototypal declarations.
* Adds tests.
--- /dev/null
+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;
+++ /dev/null
-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);
- }
- };
-};
--- /dev/null
+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);