From 88fff58b7d383d424558f72da67da98a298530fd Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Mon, 7 Feb 2011 12:36:29 +0100 Subject: [PATCH 1/1] Added all basic CSS transformations. --- lib/purify.js | 55 +++++++++++++++ test/purifier-test.js | 159 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 lib/purify.js create mode 100644 test/purifier-test.js diff --git a/lib/purify.js b/lib/purify.js new file mode 100644 index 00000000..e40ba5a8 --- /dev/null +++ b/lib/purify.js @@ -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 index 00000000..8abec44f --- /dev/null +++ b/test/purifier-test.js @@ -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 -- 2.34.1