UNPKG

11.3 kBJavaScriptView Raw
1
2/*!
3 * Stylus - utils
4 * Copyright(c) 2010 LearnBoost <dev@learnboost.com>
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var nodes = require('./nodes')
13 , basename = require('path').basename
14 , relative = require('path').relative
15 , join = require('path').join
16 , resolve = require('path').resolve
17 , glob = require('glob')
18 , fs = require('fs');
19
20/**
21 * Check if `path` looks absolute.
22 *
23 * @param {String} path
24 * @return {Boolean}
25 * @api private
26 */
27
28exports.absolute = function(path){
29 // On Windows the path could start with a drive letter, i.e. a:\\ or two leading backslashes
30 return path.substr(0, 2) == '\\\\' || '/' === path.charAt(0) || /^[a-z]:\\/i.test(path);
31};
32
33/**
34 * Attempt to lookup `path` within `paths` from tail to head.
35 * Optionally a path to `ignore` may be passed.
36 *
37 * @param {String} path
38 * @param {String} paths
39 * @param {String} ignore
40 * @param {Boolean} resolveURL
41 * @return {String}
42 * @api private
43 */
44
45exports.lookup = function(path, paths, ignore, resolveURL){
46 var lookup
47 , method = resolveURL ? resolve : join
48 , i = paths.length;
49
50 // Absolute
51 if (exports.absolute(path)) {
52 try {
53 fs.statSync(path);
54 return path;
55 } catch (err) {
56 // Ignore, continue on
57 // to trying relative lookup.
58 // Needed for url(/images/foo.png)
59 // for example
60 }
61 }
62
63 // Relative
64 while (i--) {
65 try {
66 lookup = method(paths[i], path);
67 if (ignore == lookup) continue;
68 fs.statSync(lookup);
69 return lookup;
70 } catch (err) {
71 // Ignore
72 }
73 }
74};
75
76/**
77 * Like `utils.lookup` but uses `glob` to find files.
78 *
79 * @param {String} path
80 * @param {String} paths
81 * @param {String} ignore
82 * @return {Array}
83 * @api private
84 */
85exports.find = function(path, paths, ignore) {
86 var lookup
87 , found
88 , i = paths.length;
89
90 // Absolute
91 if (exports.absolute(path)) {
92 if ((found = glob.sync(path)).length) {
93 return found;
94 }
95 }
96
97 // Relative
98 while (i--) {
99 lookup = join(paths[i], path);
100 if (ignore == lookup) continue;
101 if ((found = glob.sync(lookup)).length) {
102 return found;
103 }
104 }
105};
106
107/**
108 * Lookup index file inside dir with given `name`.
109 *
110 * @param {String} name
111 * @return {Array}
112 * @api private
113 */
114
115exports.lookupIndex = function(name, paths, filename){
116 // foo/index.styl
117 var found = exports.find(join(name, 'index.styl'), paths, filename);
118 if (!found) {
119 // foo/foo.styl
120 found = exports.find(join(name, basename(name).replace(/\.styl/i, '') + '.styl'), paths, filename);
121 }
122 if (!found && !~name.indexOf('node_modules')) {
123 // node_modules/foo/.. or node_modules/foo.styl/..
124 found = lookupPackage(join('node_modules', name));
125 }
126 return found;
127
128 function lookupPackage(dir) {
129 var package = exports.lookup(join(dir, 'package.json'), paths, filename);
130 if (!package) {
131 return /\.styl$/i.test(dir) ? exports.lookupIndex(dir, paths, filename) : lookupPackage(dir + '.styl');
132 }
133 var main = require(relative(__dirname, package)).main;
134 if (main) {
135 found = exports.find(join(dir, main), paths, filename);
136 } else {
137 found = exports.lookupIndex(dir, paths, filename);
138 }
139 return found;
140 }
141};
142
143/**
144 * Format the given `err` with the given `options`.
145 *
146 * Options:
147 *
148 * - `filename` context filename
149 * - `context` context line count [8]
150 * - `lineno` context line number
151 * - `column` context column number
152 * - `input` input string
153 *
154 * @param {Error} err
155 * @param {Object} options
156 * @return {Error}
157 * @api private
158 */
159
160exports.formatException = function(err, options){
161 var lineno = options.lineno
162 , column = options.column
163 , filename = options.filename
164 , str = options.input
165 , context = options.context || 8
166 , context = context / 2
167 , lines = ('\n' + str).split('\n')
168 , start = Math.max(lineno - context, 1)
169 , end = Math.min(lines.length, lineno + context)
170 , pad = end.toString().length;
171
172 var context = lines.slice(start, end).map(function(line, i){
173 var curr = i + start;
174 return ' '
175 + Array(pad - curr.toString().length + 1).join(' ')
176 + curr
177 + '| '
178 + line
179 + (curr == lineno
180 ? '\n' + Array(curr.toString().length + 5 + column).join('-') + '^'
181 : '');
182 }).join('\n');
183
184 // Don't show JS stack trace for Stylus errors
185 if (err.fromStylus) err.stack = '';
186
187 err.message = filename
188 + ':' + lineno
189 + ':' + column
190 + '\n' + context
191 + '\n\n' + err.message + '\n'
192 + (err.stylusStack ? err.stylusStack + '\n' : '');
193
194 return err;
195};
196
197/**
198 * Assert that `node` is of the given `type`, or throw.
199 *
200 * @param {Node} node
201 * @param {Function} type
202 * @param {String} param
203 * @api public
204 */
205
206exports.assertType = function(node, type, param){
207 exports.assertPresent(node, param);
208 if (node.nodeName == type) return;
209 var actual = node.nodeName
210 , msg = 'expected "'
211 + param + '" to be a '
212 + type + ', but got '
213 + actual + ':' + node;
214 throw new Error('TypeError: ' + msg);
215};
216
217/**
218 * Assert that `node` is a `String` or `Ident`.
219 *
220 * @param {Node} node
221 * @param {String} param
222 * @api public
223 */
224
225exports.assertString = function(node, param){
226 exports.assertPresent(node, param);
227 switch (node.nodeName) {
228 case 'string':
229 case 'ident':
230 case 'literal':
231 return;
232 default:
233 var actual = node.nodeName
234 , msg = 'expected string, ident or literal, but got ' + actual + ':' + node;
235 throw new Error('TypeError: ' + msg);
236 }
237};
238
239/**
240 * Assert that `node` is a `RGBA` or `HSLA`.
241 *
242 * @param {Node} node
243 * @param {String} param
244 * @api public
245 */
246
247exports.assertColor = function(node, param){
248 exports.assertPresent(node, param);
249 switch (node.nodeName) {
250 case 'rgba':
251 case 'hsla':
252 return;
253 default:
254 var actual = node.nodeName
255 , msg = 'expected rgba or hsla, but got ' + actual + ':' + node;
256 throw new Error('TypeError: ' + msg);
257 }
258};
259
260/**
261 * Assert that param `name` is given, aka the `node` is passed.
262 *
263 * @param {Node} node
264 * @param {String} name
265 * @api public
266 */
267
268exports.assertPresent = function(node, name){
269 if (node) return;
270 if (name) throw new Error('"' + name + '" argument required');
271 throw new Error('argument missing');
272};
273
274/**
275 * Unwrap `expr`.
276 *
277 * Takes an expressions with length of 1
278 * such as `((1 2 3))` and unwraps it to `(1 2 3)`.
279 *
280 * @param {Expression} expr
281 * @return {Node}
282 * @api public
283 */
284
285exports.unwrap = function(expr){
286 // explicitly preserve the expression
287 if (expr.preserve) return expr;
288 if ('arguments' != expr.nodeName && 'expression' != expr.nodeName) return expr;
289 if (1 != expr.nodes.length) return expr;
290 if ('arguments' != expr.nodes[0].nodeName && 'expression' != expr.nodes[0].nodeName) return expr;
291 return exports.unwrap(expr.nodes[0]);
292};
293
294/**
295 * Coerce JavaScript values to their Stylus equivalents.
296 *
297 * @param {Mixed} val
298 * @param {Boolean} [raw]
299 * @return {Node}
300 * @api public
301 */
302
303exports.coerce = function(val, raw){
304 switch (typeof val) {
305 case 'function':
306 return val;
307 case 'string':
308 return new nodes.String(val);
309 case 'boolean':
310 return new nodes.Boolean(val);
311 case 'number':
312 return new nodes.Unit(val);
313 default:
314 if (null == val) return nodes.null;
315 if (Array.isArray(val)) return exports.coerceArray(val, raw);
316 if (val.nodeName) return val;
317 return exports.coerceObject(val, raw);
318 }
319};
320
321/**
322 * Coerce a javascript `Array` to a Stylus `Expression`.
323 *
324 * @param {Array} val
325 * @param {Boolean} [raw]
326 * @return {Expression}
327 * @api private
328 */
329
330exports.coerceArray = function(val, raw){
331 var expr = new nodes.Expression;
332 val.forEach(function(val){
333 expr.push(exports.coerce(val, raw));
334 });
335 return expr;
336};
337
338/**
339 * Coerce a javascript object to a Stylus `Expression` or `Object`.
340 *
341 * For example `{ foo: 'bar', bar: 'baz' }` would become
342 * the expression `(foo 'bar') (bar 'baz')`. If `raw` is true
343 * given `obj` would become a Stylus hash object.
344 *
345 * @param {Object} obj
346 * @param {Boolean} [raw]
347 * @return {Expression|Object}
348 * @api public
349 */
350
351exports.coerceObject = function(obj, raw){
352 var node = raw ? new nodes.Object : new nodes.Expression
353 , val;
354
355 for (var key in obj) {
356 val = exports.coerce(obj[key], raw);
357 key = new nodes.Ident(key);
358 if (raw) {
359 node.set(key, val);
360 } else {
361 node.push(exports.coerceArray([key, val]));
362 }
363 }
364
365 return node;
366};
367
368/**
369 * Return param names for `fn`.
370 *
371 * @param {Function} fn
372 * @return {Array}
373 * @api private
374 */
375
376exports.params = function(fn){
377 return fn
378 .toString()
379 .match(/\(([^)]*)\)/)[1].split(/ *, */);
380};
381
382/**
383 * Merge object `b` with `a`.
384 *
385 * @param {Object} a
386 * @param {Object} b
387 * @return {Object} a
388 * @api private
389 */
390
391exports.merge = function(a, b){
392 for (var k in b) a[k] = b[k];
393 return a;
394};
395
396/**
397 * Returns an array with unique values.
398 *
399 * @param {Array} arr
400 * @return {Array}
401 * @api private
402 */
403
404exports.uniq = function(arr){
405 var obj = {}
406 , ret = [];
407
408 for (var i = 0, len = arr.length; i < len; ++i) {
409 if (arr[i] in obj) continue;
410
411 obj[arr[i]] = true;
412 ret.push(arr[i]);
413 }
414 return ret;
415};
416
417/**
418 * Compile selector strings in `arr` from the bottom-up
419 * to produce the selector combinations. For example
420 * the following Stylus:
421 *
422 * ul
423 * li
424 * p
425 * a
426 * color: red
427 *
428 * Would return:
429 *
430 * [ 'ul li a', 'ul p a' ]
431 *
432 * @param {Array} arr
433 * @param {Boolean} leaveHidden
434 * @return {Array}
435 * @api private
436 */
437
438var HIDDEN_SELECTOR_RX = /^\s*\/?\$/
439 , ROOT_SELECTOR_RX = /^\//g
440 , PARENT_SELECTOR_RX = /^&|([^\\])&/g
441 , ESCAPED_PARENT_SELECTOR_RX = /\\&/g;
442
443exports.compileSelectors = function(arr, leaveHidden){
444 var self = this
445 , selectors = []
446 , buf = [];
447
448 function interpolateParent(selector, buf) {
449 var str = selector.val.replace(ROOT_SELECTOR_RX, '').trim();
450 if (buf.length) {
451 for (var i = 0, len = buf.length; i < len; ++i) {
452 if (~buf[i].indexOf('&') || '/' === buf[i].charAt(0)) {
453 str = buf[i].replace(PARENT_SELECTOR_RX, '$1' + str).replace(ROOT_SELECTOR_RX, '').trim();
454 } else {
455 str += ' ' + buf[i].trim();
456 }
457 }
458 }
459 return str.trim();
460 }
461
462 function compile(arr, i) {
463 if (i) {
464 arr[i].forEach(function(selector){
465 if (!leaveHidden && selector.val.match(HIDDEN_SELECTOR_RX)) return;
466 if (selector.inherits) {
467 buf.unshift(selector.val);
468 compile(arr, i - 1);
469 buf.shift();
470 } else {
471 selectors.push(interpolateParent(selector, buf));
472 }
473 });
474 } else {
475 arr[0].forEach(function(selector){
476 if (!leaveHidden && selector.val.match(HIDDEN_SELECTOR_RX)) return;
477 var str = interpolateParent(selector, buf);
478 if (~str.indexOf('&')) str = str.replace(PARENT_SELECTOR_RX, '$1').replace(ESCAPED_PARENT_SELECTOR_RX, '&').trim();
479 if (!str.length) return;
480 selectors.push((self.indent || '') + str.trimRight());
481 });
482 }
483 }
484
485 compile(arr, arr.length - 1);
486
487 // Return the list with unique selectors only
488 return exports.uniq(selectors);
489};