Improves ExpressionsProcessor.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Wed, 24 Sep 2014 19:56:25 +0000 (20:56 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Fri, 10 Oct 2014 20:22:43 +0000 (21:22 +0100)
* Adds prototypal declarations.
* Adds tests.

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

diff --git a/lib/text/expressions-processor.js b/lib/text/expressions-processor.js
new file mode 100644 (file)
index 0000000..02e72ea
--- /dev/null
@@ -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 (file)
index b231d85..0000000
+++ /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 (file)
index 0000000..798de3b
--- /dev/null
@@ -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 = \'&#xf187;\')}',
+        'a{zoom:__ESCAPED_EXPRESSION_CLEAN_CSS0__}',
+        'a{zoom:expression(this.runtimeStyle[\"zoom\"] = \'1\', this.innerHTML = \'&#xf187;\')}'
+      ],
+      '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);