1 var util = require('util');
14 process: function(data, options) {
16 replace = function(pattern, replacement) {
17 if (typeof arguments[0] == 'function')
20 data = data.replace.apply(data, arguments);
23 options = options || {};
27 var originalReplace = replace;
28 replace = function(pattern, replacement) {
29 var name = typeof pattern == 'function' ?
30 /function (\w+)\(/.exec(pattern.toString())[1] :
33 originalReplace(pattern, replacement);
34 console.timeEnd(name);
38 // strip comments one by one
39 replace(function stripComments() {
40 data = self.stripComments(data);
43 // replace content: with a placeholder
44 replace(function stripContent() {
45 data = self.stripContent(data);
48 replace(/;\s*;+/g, ';') // whitespace between semicolons & multiple semicolons
49 replace(/\n/g, '') // line breaks
50 replace(/\s+/g, ' ') // multiple whitespace
51 replace(/ !important/g, '!important') // whitespace before !important
52 replace(/[ ]?,[ ]?/g, ',') // space with a comma
53 replace(/progid:[^(]+\(([^\)]+)/g, function(match, contents) { // restore spaces inside IE filters (IE 7 issue)
54 return match.replace(/,/g, ', ');
56 replace(/ ([+~>]) /g, '$1') // replace spaces around selectors
57 replace(/\{([^}]+)\}/g, function(match, contents) { // whitespace inside content
58 return '{' + contents.trim().replace(/(\s*)([;:=\s])(\s*)/g, '$2') + '}';
60 replace(/;}/g, '}') // trailing semicolons
61 replace(/rgb\s*\(([^\)]+)\)/g, function(match, color) { // rgb to hex colors
62 var parts = color.split(',');
64 for (var i = 0; i < 3; i++) {
65 var asHex = parseInt(parts[i], 10).toString(16);
66 encoded += asHex.length == 1 ? '0' + asHex : asHex;
70 replace(/([^"'=\s])\s*#([0-9a-f]{6})/gi, function(match, prefix, color) { // long hex to short hex
71 if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5])
72 return (prefix + (/:$/.test(prefix) ? '' : ' ')) + '#' + color[0] + color[2] + color[4];
74 return (prefix + (/:$/.test(prefix) ? '' : ' ')) + '#' + color;
76 replace(/(color|background):(\w+)/g, function(match, property, colorName) { // replace standard colors with hex values (only if color name is longer then hex value)
77 if (CleanCSS.colors[colorName]) return property + ':' + CleanCSS.colors[colorName];
80 replace(/([: ,\(])#f00/g, '$1red') // replace #f00 with red as it's shorter
81 replace(/font\-weight:(\w+)/g, function(match, weight) { // replace font weight with numerical value
82 if (weight == 'normal') return 'font-weight:400';
83 else if (weight == 'bold') return 'font-weight:700';
86 replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\([^\)]+\))([;}'"])/g, function(match, filter, args, suffix) { // IE shorter filters but only if single (IE 7 issue)
87 return filter.toLowerCase() + args + suffix;
89 replace(/(\s|:)0(px|em|ex|cm|mm|in|pt|pc|%)/g, '$1' + '0') // zero + unit to zero
90 replace(/(border|border-top|border-right|border-bottom|border-left|outline):none/g, '$1:0') // none to 0
91 replace(/(background):none([;}])/g, '$1:0$2') // background:none to 0
92 replace(/0 0 0 0([^\.])/g, '0$1') // multiple zeros into one
93 replace(/([: ,=\-])0\.(\d)/g, '$1.$2')
94 if (options.removeEmpty) replace(/[^}]+?{\s*?}/g, '') // empty elements
95 if (data.indexOf('charset') > 0) replace(/(.+)(@charset [^;]+;)/, '$2$1') // move first charset to the beginning
96 replace(/(.)(@charset [^;]+;)/g, '$1') // remove all extra charsets that are not at the beginning
97 replace(/\*([\.#:\[])/g, '$1') // remove universal selector when not needed (*#id, *.class etc)
98 replace(/ {/g, '{') // whitespace before definition
99 replace(/\} /g, '}') // whitespace after definition
101 // Get the special comments, content content, and spaces inside calc back
102 replace(/calc\([^\}]+\}/g, function(match) {
103 return match.replace(/\+/g, ' + ');
105 replace(/__CSSCOMMENT__/g, function() { return self.specialComments.shift(); });
106 replace(/__CSSCONTENT__/g, function() { return self.contentBlocks.shift(); });
108 return data.trim() // trim spaces at beginning and end
111 // Strips special comments (/*! ... */) by replacing them by __CSSCOMMENT__ marker
112 // for further restoring. Plain comments are removed. It's done by scanning datq using
113 // String#indexOf scanning instead of regexps to speed up the process.
114 stripComments: function(data) {
120 for (; nextEnd < data.length; ) {
121 nextStart = data.indexOf('/*', nextEnd);
122 nextEnd = data.indexOf('*/', nextStart);
123 if (nextStart == -1 || nextEnd == -1) break;
125 tempData.push(data.substring(cursor, nextStart))
126 if (data[nextStart + 2] == '!') {
127 // in case of special comments, replace them with a placeholder
128 this.specialComments.push(data.substring(nextStart, nextEnd + 2));
129 tempData.push('__CSSCOMMENT__');
131 cursor = nextEnd + 2;
134 return tempData.length > 0 ?
135 tempData.join('') + data.substring(cursor, data.length) :
139 // Strips content tags by replacing them by __CSSCONTENT__ marker
140 // for further restoring. It's done via string scanning instead of
141 // regexps to speed up the process.
142 stripContent: function(data) {
148 matchedParenthesis = null;
150 // Finds either first (matchedParenthesis == null) or second matching parenthesis
151 // so we can determine boundaries of content block.
152 var nextParenthesis = function(pos) {
156 if (matchedParenthesis) {
157 min = data.indexOf(matchedParenthesis, pos);
158 if (min == -1) min = max;
160 var next1 = data.indexOf("'", pos);
161 var next2 = data.indexOf('"', pos);
162 if (next1 == -1) next1 = max;
163 if (next2 == -1) next2 = max;
165 min = next1 > next2 ? next2 : next1;
168 if (min == max) return -1;
170 if (matchedParenthesis) {
171 matchedParenthesis = null;
174 // check if there's anything else between pos and min that doesn't match ':' or whitespace
175 if (/[^:\s]/.test(data.substring(pos, min))) return -1;
176 matchedParenthesis = data.charAt(min);
181 for (; nextEnd < data.length; ) {
182 nextStart = data.indexOf('content', nextEnd);
183 if (nextStart == -1) break;
185 nextStart = nextParenthesis(nextStart + 7);
186 nextEnd = nextParenthesis(nextStart);
187 if (nextStart == -1 || nextEnd == -1) break;
189 tempData.push(data.substring(cursor, nextStart - 1));
190 tempData.push('__CSSCONTENT__');
191 this.contentBlocks.push(data.substring(nextStart - 1, nextEnd + 1));
192 cursor = nextEnd + 1;
195 return tempData.length > 0 ?
196 tempData.join('') + data.substring(cursor, data.length) :
201 module.exports = CleanCSS;