Added all basic CSS transformations.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 7 Feb 2011 11:36:29 +0000 (12:36 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 7 Feb 2011 11:36:29 +0000 (12:36 +0100)
lib/purify.js [new file with mode: 0644]
test/purifier-test.js [new file with mode: 0644]

diff --git a/lib/purify.js b/lib/purify.js
new file mode 100644 (file)
index 0000000..e40ba5a
--- /dev/null
@@ -0,0 +1,55 @@
+var Purify = {
+  process: function(data) {
+    // strip comments one by one
+    for (var end = 0; end < data.length; ) {
+      var start = data.indexOf('/*', end);
+      if (data[start + 2] == '!') { // skip special comments: /*!...*/
+        end = start + 1;
+        continue;
+      }
+      
+      end = data.indexOf('*/', start);
+      if (start == -1 || end == -1) break;
+      
+      data = data.substring(0, start) + data.substring(end + 2);
+      end = start;
+    }
+    
+    return data
+      .replace(/;\s*;/g, ';;') // whitespace between semicolons
+      .replace(/;+/g, ';') // multiple semicolons
+      .replace(/,[ ]+/g, ',') // comma
+      .replace(/\s+/g, ' ') // whitespace
+      .replace(/\{([^}]+)\}/g, function(match, contents) { // whitespace inside content
+        return '{' + contents.trim().replace(/(\s*)([;:=\s])(\s*)/g, '$2') + '}';
+      })
+      .replace(/;}/g, '}') // trailing semicolons
+      .replace(/rgb\s*\(([^\)]+)\)/g, function(match, color) { // rgb to hex colors
+        var parts = color.split(',');
+        var encoded = '#';
+        for (var i = 0; i < 3; i++) {
+          var asHex = parseInt(parts[i], 10).toString(16);
+          encoded += asHex.length == 1 ? '0' + asHex : asHex;
+        }
+        return encoded;
+      })
+      .replace(/([^"'=\s])\s*#([0-9a-f]{6})/gi, function(match, prefix, color) { // long hex to short hex
+        if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5])
+          return prefix + '#' + color[0] + color[2] + color[4];
+        else
+          return prefix + '#' + color;
+      })
+      .replace(/progid:DXImageTransform\.Microsoft\.Alpha/g, 'alpha') // IE alpha filter
+      .replace(/(\s|:)0(px|em|ex|cm|mm|in|pt|pc|%)/g, '$1' + '0') // zero + unit to zero
+      .replace(/none/g, '0') // none to 0
+      .replace(/( 0){1,4}/g, '') // multiple zeros into one
+      .replace(/([: ,])0\.(\d)+/g, '$1.$2')
+      .replace(/[^\}]+{(;)*}/g, '') // empty elements
+      .replace(/(.+)(@charset [^;]+;)/, '$2$1')
+      .replace(/(.+)(@charset [^;]+;)/g, '$1')
+      .replace(/ {/g, '{') // whitespace before definition
+      .replace(/\} /g, '}') // whitespace after definition
+  }
+};
+
+exports.Purify = Purify;
\ No newline at end of file
diff --git a/test/purifier-test.js b/test/purifier-test.js
new file mode 100644 (file)
index 0000000..8abec44
--- /dev/null
@@ -0,0 +1,159 @@
+var vows = require('vows'),
+  assert = require('assert');
+
+var Purify = require('../lib/purify').Purify;
+
+var cssContext = function(groups) {
+  var context = {};
+  var clean = function(cleanCss) {
+    return function(css) { assert.equal(Purify.process(css), cleanCss); }
+  };
+  
+  for (var g in groups) {
+    var transformation = groups[g];
+    if (typeof transformation == 'string') transformation = [transformation, transformation];
+    
+    context[g] = {
+      topic: transformation[0],
+      clean: clean(transformation[1])
+    };
+  }
+  
+  return context;
+};
+
+vows.describe('purify').addBatch({
+  'identity': cssContext({
+    'preserve minified content': 'a{color:#f00}'
+  }),
+  'semicolons': cssContext({
+    'multiple semicolons': [
+      'a{color:#fff;;;width:0; ;}',
+      'a{color:#fff;width:0}'
+    ],
+    'trailing semicolon': [
+      'a{color:#fff;}',
+      'a{color:#fff}'
+    ],
+    'trailing semicolon and space': [
+      'a{color:#fff ; }',
+      'a{color:#fff}'
+    ],
+    'comma and space': [
+      'a{color:rgba(0, 0,  5, .5)}',
+      'a{color:rgba(0,0,5,.5)}'
+    ]
+  }),
+  'whitespace': cssContext({
+    'one argument': [
+      'div  a  { color:#fff  }',
+      'div a{color:#fff}'
+    ],
+    'line breaks': [
+      'div \na\r\n { width:500px }',
+      'div a{width:500px}'
+    ],
+    'multiple arguments': [
+      'a{color:#fff ;  font-weight:  bold }',
+      'a{color:#fff;font-weight:bold}'
+    ],
+    'space delimited arguments': [
+      'a {border: 1px solid #f00; margin: 0 auto }',
+      'a{border:1px solid #f00;margin:0 auto}'
+    ]
+  }),
+  'empty elements': cssContext({
+    'single': [
+      ' div p {  \n}',
+      ''
+    ],
+    'between non-empty': [
+      'div {color:#fff}  a{  } p{  line-height:1.35em}',
+      'div{color:#fff}p{line-height:1.35em}'
+    ],
+    'just a semicolon': [
+      'div { ; }',
+      ''
+    ]
+  }),
+  'comments': cssContext({
+    'single line': [
+      'a{color:#fff}/* some comment*/p{height:10px/* other comment */}',
+      'a{color:#fff}p{height:10px}'
+    ],
+    'multiline': [
+      '/* \r\n multiline \n comment */a{color:rgba(0,0,0,0.8)}',
+      'a{color:rgba(0,0,0,.8)}'
+    ],
+    'comment chars in comments': [
+      '/* \r\n comment chars * inside / comments */a{color:#fff}',
+      'a{color:#fff}'
+    ],
+    'comment inside block': [
+      'a{/* \r\n some comments */color:#fff}',
+      'a{color:#fff}'
+    ],
+    'special comments': [
+      '/*! special comment */a{color:#f00} /* normal comment */',
+      '/*! special comment */a{color:#f00}'
+    ]
+  }),
+  'zero values': cssContext({
+    'with units': [
+      'a{margin:0px 0pt 0em 0%;padding: 0in 0cm 0mm 0pc;border-top-width:0ex}',
+      'a{margin:0;padding:0;border-top-width:0}'
+    ],
+    'multiple into one': [
+      'a{margin:0 0 0 0;padding:0 0 0;border-width:0 0}',
+      'a{margin:0;padding:0;border-width:0}'
+    ],
+    'none to zeros': [
+      'a{border:none;background:none}',
+      'a{border:0;background:0}'
+    ]
+  }),
+  'floats': cssContext({
+    'strips zero in fractions': [
+      'a{ margin-bottom: 0.5em}',
+      'a{margin-bottom:.5em}'
+    ],
+    'not strips zero in fractions of numbers greater than zero': [
+      'a{ margin-bottom: 20.5em}',
+      'a{margin-bottom:20.5em}'
+    ]
+  }),
+  'colors': cssContext({
+    'shorten rgb to standard hexadecimal format': [
+      'a{ color:rgb (5, 10, 15) }',
+      'a{color:#050a0f}'
+    ],
+    'skip rgba shortening': [
+      'a{ color:rgba(5, 10, 15, 0.5)}',
+      'a{color:rgba(5,10,15,.5)}'
+    ],
+    'shorten colors to 3 digit hex instead of 6 digit': [
+      'a{ background-color: #ff0000; color:rgb(0, 17, 255)}',
+      'a{background-color:#f00;color:#01f}'
+    ],
+    'skip shortening IE filter colors': [
+      'a{ filter: chroma(color = "#ff0000")}',
+      'a{filter:chroma(color="#ff0000")}'
+    ]
+  }),
+  'ie filters': cssContext({
+    'alpha': [
+      "a{ filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80); -ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)';}",
+      "a{filter:alpha(Opacity=80);-ms-filter:'alpha(Opacity=50)'}"
+    ]
+  }),
+  'charsets': cssContext({
+    'not at beginning': [
+      "a{ color: #f00; }@charset 'utf-8';b { font-weight: bold}",
+      "@charset 'utf-8';a{color:#f00}b{font-weight:bold}"
+    ],
+    'multiple charsets': [
+      "@charset 'utf-8';div :before { display: block }@charset 'utf-8';a { color: #f00 }",
+      "@charset 'utf-8';div :before{display:block}a{color:#f00}"
+    ]
+  })
+}).export(module);
\ No newline at end of file