2.0_beta sync to rsa
[framework/web/web-ui-fw.git] / build-tools / lib / cleancss / clean.js
1 var util = require('util');
2
3 var CleanCSS = {
4   colors: {
5     white: '#fff',
6     black: '#000',
7     fuchsia: '#f0f',
8     yellow: '#ff0'
9   },
10
11   specialComments: [],
12   contentBlocks: [],
13
14   process: function(data, options) {
15     var self = this,
16       replace = function(pattern, replacement) {
17         if (typeof arguments[0] == 'function')
18           arguments[0]();
19         else
20           data = data.replace.apply(data, arguments);
21       };
22
23     options = options || {};
24
25     // replace function
26     if (options.debug) {
27       var originalReplace = replace;
28       replace = function(pattern, replacement) {
29         var name = typeof pattern == 'function' ?
30           /function (\w+)\(/.exec(pattern.toString())[1] :
31           pattern;
32         console.time(name);
33         originalReplace(pattern, replacement);
34         console.timeEnd(name);
35       };
36     }
37
38     // strip comments one by one
39     replace(function stripComments() {
40       data = self.stripComments(data);
41     });
42
43     // replace content: with a placeholder
44     replace(function stripContent() {
45       data = self.stripContent(data);
46     });
47
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, ', ');
55     })
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') + '}';
59     })
60     replace(/;}/g, '}') // trailing semicolons
61     replace(/rgb\s*\(([^\)]+)\)/g, function(match, color) { // rgb to hex colors
62       var parts = color.split(',');
63       var encoded = '#';
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;
67       }
68       return encoded;
69     })
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];
73       else
74         return (prefix + (/:$/.test(prefix) ? '' : ' ')) + '#' + color;
75     })
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];
78       else return match;
79     })
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';
84       else return match;
85     })
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;
88     })
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
100
101     // Get the special comments, content content, and spaces inside calc back
102     replace(/calc\([^\}]+\}/g, function(match) {
103       return match.replace(/\+/g, ' + ');
104     });
105     replace(/__CSSCOMMENT__/g, function() { return self.specialComments.shift(); });
106     replace(/__CSSCONTENT__/g, function() { return self.contentBlocks.shift(); });
107
108     return data.trim() // trim spaces at beginning and end
109   },
110
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) {
115     var tempData = [],
116       nextStart = 0,
117       nextEnd = 0,
118       cursor = 0;
119
120     for (; nextEnd < data.length; ) {
121       nextStart = data.indexOf('/*', nextEnd);
122       nextEnd = data.indexOf('*/', nextStart);
123       if (nextStart == -1 || nextEnd == -1) break;
124
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__');
130       }
131       cursor = nextEnd + 2;
132     }
133
134     return tempData.length > 0 ?
135       tempData.join('') + data.substring(cursor, data.length) :
136       data;
137   },
138
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) {
143     var tempData = [],
144       nextStart = 0,
145       nextEnd = 0,
146       tempStart = 0,
147       cursor = 0,
148       matchedParenthesis = null;
149
150     // Finds either first (matchedParenthesis == null) or second matching parenthesis
151     // so we can determine boundaries of content block.
152     var nextParenthesis = function(pos) {
153       var min,
154         max = data.length;
155
156       if (matchedParenthesis) {
157         min = data.indexOf(matchedParenthesis, pos);
158         if (min == -1) min = max;
159       } else {
160         var next1 = data.indexOf("'", pos);
161         var next2 = data.indexOf('"', pos);
162         if (next1 == -1) next1 = max;
163         if (next2 == -1) next2 = max;
164
165         min = next1 > next2 ? next2 : next1;
166       }
167
168       if (min == max) return -1;
169
170       if (matchedParenthesis) {
171         matchedParenthesis = null;
172         return min;
173       } else {
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);
177         return min + 1;
178       }
179     };
180
181     for (; nextEnd < data.length; ) {
182       nextStart = data.indexOf('content', nextEnd);
183       if (nextStart == -1) break;
184
185       nextStart = nextParenthesis(nextStart + 7);
186       nextEnd = nextParenthesis(nextStart);
187       if (nextStart == -1 || nextEnd == -1) break;
188
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;
193     }
194
195     return tempData.length > 0 ?
196       tempData.join('') + data.substring(cursor, data.length) :
197       data;
198   }
199 };
200
201 module.exports = CleanCSS;