1 |
|
2 |
|
3 |
|
4 | 'use strict';
|
5 |
|
6 | var cssParser = require('css').parse,
|
7 | debug = require('debug')('analyze-css'),
|
8 | fs = require('fs'),
|
9 | path = require('path'),
|
10 | preprocessors = new(require('./preprocessors'))(),
|
11 | slickParse = require('slick').parse,
|
12 | VERSION = require('./../package').version;
|
13 |
|
14 | function analyzer(css, options, callback) {
|
15 | var res;
|
16 |
|
17 |
|
18 | if (typeof options === 'function') {
|
19 | callback = options;
|
20 | options = {};
|
21 | }
|
22 |
|
23 | this.options = options;
|
24 | debug('opts: %j', this.options);
|
25 |
|
26 | if (typeof css !== 'string') {
|
27 | callback(this.error('css parameter passed is not a string!', analyzer.EXIT_CSS_PASSED_IS_NOT_STRING), null);
|
28 | return;
|
29 | }
|
30 |
|
31 |
|
32 | if (typeof options.preprocessor === 'string') {
|
33 | debug('Using "%s" preprocessor', options.preprocessor);
|
34 |
|
35 | var preprocessor = preprocessors.get(options.preprocessor);
|
36 |
|
37 | try {
|
38 | css = preprocessor.process(css, options);
|
39 | } catch (ex) {
|
40 | throw new Error('Preprocessing failed: ' + ex);
|
41 | }
|
42 |
|
43 | debug('Preprocessing completed');
|
44 | }
|
45 |
|
46 | res = this.analyze(css);
|
47 |
|
48 |
|
49 | if (res !== true) {
|
50 | callback(res, null);
|
51 | return;
|
52 | }
|
53 |
|
54 |
|
55 | res = {
|
56 | generator: 'analyze-css v' + VERSION,
|
57 | metrics: this.metrics,
|
58 | };
|
59 |
|
60 |
|
61 | if (options.noOffenders !== true) {
|
62 | res.offenders = this.offenders;
|
63 | }
|
64 |
|
65 | callback(null, res);
|
66 | }
|
67 |
|
68 | analyzer.version = VERSION;
|
69 |
|
70 |
|
71 | analyzer.path = path.normalize(__dirname + '/..');
|
72 | analyzer.pathBin = analyzer.path + '/bin/analyze-css.js';
|
73 |
|
74 |
|
75 | analyzer.EXIT_NEED_OPTIONS = 2;
|
76 | analyzer.EXIT_PARSING_FAILED = 251;
|
77 | analyzer.EXIT_EMPTY_CSS = 252;
|
78 | analyzer.EXIT_CSS_PASSED_IS_NOT_STRING = 253;
|
79 | analyzer.EXIT_URL_LOADING_FAILED = 254;
|
80 | analyzer.EXIT_FILE_LOADING_FAILED = 255;
|
81 |
|
82 | analyzer.prototype = {
|
83 | emitter: false,
|
84 | tree: false,
|
85 |
|
86 | metrics: {},
|
87 | offenders: {},
|
88 |
|
89 | error: function(msg, code) {
|
90 | var err = new Error(msg);
|
91 | err.code = code;
|
92 |
|
93 | return err;
|
94 | },
|
95 |
|
96 |
|
97 | emit: function( /* eventName, arg1, arg2, ... */ ) {
|
98 |
|
99 | this.emitter.emit.apply(this.emitter, arguments);
|
100 | },
|
101 |
|
102 |
|
103 | on: function(ev, fn) {
|
104 | this.emitter.on(ev, fn);
|
105 | },
|
106 |
|
107 | setMetric: function(name, value) {
|
108 | value = value || 0;
|
109 |
|
110 |
|
111 | this.metrics[name] = value;
|
112 | },
|
113 |
|
114 |
|
115 | incrMetric: function(name, incr /* =1 */ ) {
|
116 | var currVal = this.metrics[name] || 0;
|
117 | incr = incr || 1;
|
118 |
|
119 |
|
120 | this.setMetric(name, currVal + incr);
|
121 | },
|
122 |
|
123 | addOffender: function(metricName, msg, position /* = undefined */ ) {
|
124 | if (typeof this.offenders[metricName] === 'undefined') {
|
125 | this.offenders[metricName] = [];
|
126 | }
|
127 |
|
128 | this.offenders[metricName].push({
|
129 | 'message': msg,
|
130 | 'position': position || this.currentPosition
|
131 | });
|
132 | },
|
133 |
|
134 | setCurrentPosition: function(position) {
|
135 | this.currentPosition = position;
|
136 | },
|
137 |
|
138 | initRules: function() {
|
139 | var debug = require('debug')('analyze-css:rules'),
|
140 | re = /\.js$/,
|
141 | rules = [];
|
142 |
|
143 |
|
144 | this.emitter = new(require('events').EventEmitter)();
|
145 | this.emitter.setMaxListeners(200);
|
146 |
|
147 |
|
148 | rules = fs.readdirSync(fs.realpathSync(__dirname + '/../rules/'))
|
149 |
|
150 | .filter(function(file) {
|
151 | return re.test(file);
|
152 | })
|
153 |
|
154 | .map(function(file) {
|
155 | return file.replace(re, '');
|
156 | });
|
157 |
|
158 | debug('Rules to be loaded: %s', rules.join(', '));
|
159 |
|
160 | rules.forEach(function(name) {
|
161 | var rule = require('./../rules/' + name);
|
162 | rule(this);
|
163 |
|
164 | debug('"%s" loaded: %s', name, rule.description || '-');
|
165 | }, this);
|
166 | },
|
167 |
|
168 | parseCss: function(css) {
|
169 | var debug = require('debug')('analyze-css:parser');
|
170 | debug('Going to parse %s kB of CSS', (css.length / 1024).toFixed(2));
|
171 |
|
172 | if (css.trim() === '') {
|
173 | return this.error('Empty CSS was provided', analyzer.EXIT_EMPTY_CSS);
|
174 | }
|
175 |
|
176 | this.tree = cssParser(css, {
|
177 |
|
178 | silent: true
|
179 | });
|
180 |
|
181 | debug('CSS parsed');
|
182 | return true;
|
183 | },
|
184 |
|
185 | parseRules: function(rules) {
|
186 | rules.forEach(function(rule, idx) {
|
187 | debug('%j', rule);
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 | this.setCurrentPosition(rule.position);
|
194 |
|
195 | switch (rule.type) {
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | case 'media':
|
202 | this.emit('media', rule.media, rule.rules);
|
203 |
|
204 |
|
205 | if (rule.rules) {
|
206 | this.parseRules(rule.rules);
|
207 | }
|
208 |
|
209 | this.emit('mediaEnd', rule.media, rule.rules);
|
210 | break;
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | case 'rule':
|
218 | if (!rule.selectors || !rule.declarations) {
|
219 | return;
|
220 | }
|
221 |
|
222 | this.emit('rule', rule);
|
223 |
|
224 |
|
225 | rule.selectors.forEach(function(selector) {
|
226 | var parsedSelector,
|
227 | expressions = [],
|
228 | i, len;
|
229 |
|
230 |
|
231 |
|
232 |
|
233 | parsedSelector = slickParse(selector)[0];
|
234 |
|
235 | if (typeof parsedSelector === 'undefined') {
|
236 | var positionDump = "Rule position start @ " + rule.position.start.line + ':' + rule.position.start.column + ", end @ " + rule.position.end.line + ':' + rule.position.end.column;
|
237 | throw this.error('Unable to parse "' + selector + '" selector. ' + positionDump, analyzer.EXIT_PARSING_FAILED);
|
238 | }
|
239 |
|
240 |
|
241 | for (i = 0, len = parsedSelector.length; i < len; i++) {
|
242 | expressions.push(parsedSelector[i]);
|
243 | }
|
244 |
|
245 | this.emit('selector', rule, selector, expressions);
|
246 |
|
247 | expressions.forEach(function(expression) {
|
248 | this.emit('expression', selector, expression);
|
249 | }, this);
|
250 | }, this);
|
251 |
|
252 | rule.declarations.forEach(function(declaration) {
|
253 | this.setCurrentPosition(declaration.position);
|
254 |
|
255 | switch (declaration.type) {
|
256 | case 'declaration':
|
257 | this.emit('declaration', rule, declaration.property, declaration.value);
|
258 | break;
|
259 |
|
260 | case 'comment':
|
261 | this.emit('comment', declaration.comment);
|
262 | break;
|
263 | }
|
264 | }, this);
|
265 | break;
|
266 |
|
267 |
|
268 | case 'comment':
|
269 | this.emit('comment', rule.comment);
|
270 | break;
|
271 |
|
272 |
|
273 | case 'font-face':
|
274 | this.emit('font-face', rule);
|
275 | break;
|
276 |
|
277 |
|
278 | case 'import':
|
279 | this.emit('import', rule.import);
|
280 | break;
|
281 | }
|
282 | }, this);
|
283 | },
|
284 |
|
285 | run: function() {
|
286 | var stylesheet = this.tree && this.tree.stylesheet,
|
287 | rules = stylesheet && stylesheet.rules;
|
288 |
|
289 | this.emit('stylesheet', stylesheet);
|
290 |
|
291 |
|
292 | stylesheet.parsingErrors.forEach(function(err) {
|
293 | debug('error: %j', err);
|
294 |
|
295 | var pos = {
|
296 | line: err.line,
|
297 | column: err.column
|
298 | };
|
299 | this.setCurrentPosition({
|
300 | start: pos,
|
301 | end: pos
|
302 | });
|
303 |
|
304 | this.emit('error', err);
|
305 | }, this);
|
306 |
|
307 | this.parseRules(rules);
|
308 | },
|
309 |
|
310 | analyze: function(css) {
|
311 | var res,
|
312 | then = Date.now();
|
313 |
|
314 | this.metrics = {};
|
315 | this.offenders = {};
|
316 |
|
317 |
|
318 | this.initRules();
|
319 |
|
320 |
|
321 | res = this.parseCss(css);
|
322 |
|
323 | if (res !== true) {
|
324 | return res;
|
325 | }
|
326 |
|
327 | this.emit('css', css);
|
328 |
|
329 |
|
330 | try {
|
331 | this.run();
|
332 | } catch (ex) {
|
333 | return ex;
|
334 | }
|
335 |
|
336 | this.emit('report');
|
337 |
|
338 | debug('Completed in %d ms', Date.now() - then);
|
339 | return true;
|
340 | }
|
341 | };
|
342 |
|
343 | module.exports = analyzer;
|