UNPKG

11.2 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 err.message = filename
185 + ':' + lineno
186 + ':' + column
187 + '\n' + context
188 + '\n\n' + err.message + '\n'
189 + (err.stylusStack ? err.stylusStack + '\n' : '');
190
191 return err;
192};
193
194/**
195 * Assert that `node` is of the given `type`, or throw.
196 *
197 * @param {Node} node
198 * @param {Function} type
199 * @param {String} param
200 * @api public
201 */
202
203exports.assertType = function(node, type, param){
204 exports.assertPresent(node, param);
205 if (node.nodeName == type) return;
206 var actual = node.nodeName
207 , msg = 'expected "'
208 + param + '" to be a '
209 + type + ', but got '
210 + actual + ':' + node;
211 throw new Error('TypeError: ' + msg);
212};
213
214/**
215 * Assert that `node` is a `String` or `Ident`.
216 *
217 * @param {Node} node
218 * @param {String} param
219 * @api public
220 */
221
222exports.assertString = function(node, param){
223 exports.assertPresent(node, param);
224 switch (node.nodeName) {
225 case 'string':
226 case 'ident':
227 case 'literal':
228 return;
229 default:
230 var actual = node.nodeName
231 , msg = 'expected string, ident or literal, but got ' + actual + ':' + node;
232 throw new Error('TypeError: ' + msg);
233 }
234};
235
236/**
237 * Assert that `node` is a `RGBA` or `HSLA`.
238 *
239 * @param {Node} node
240 * @param {String} param
241 * @api public
242 */
243
244exports.assertColor = function(node, param){
245 exports.assertPresent(node, param);
246 switch (node.nodeName) {
247 case 'rgba':
248 case 'hsla':
249 return;
250 default:
251 var actual = node.nodeName
252 , msg = 'expected rgba or hsla, but got ' + actual + ':' + node;
253 throw new Error('TypeError: ' + msg);
254 }
255};
256
257/**
258 * Assert that param `name` is given, aka the `node` is passed.
259 *
260 * @param {Node} node
261 * @param {String} name
262 * @api public
263 */
264
265exports.assertPresent = function(node, name){
266 if (node) return;
267 if (name) throw new Error('"' + name + '" argument required');
268 throw new Error('argument missing');
269};
270
271/**
272 * Unwrap `expr`.
273 *
274 * Takes an expressions with length of 1
275 * such as `((1 2 3))` and unwraps it to `(1 2 3)`.
276 *
277 * @param {Expression} expr
278 * @return {Node}
279 * @api public
280 */
281
282exports.unwrap = function(expr){
283 // explicitly preserve the expression
284 if (expr.preserve) return expr;
285 if ('arguments' != expr.nodeName && 'expression' != expr.nodeName) return expr;
286 if (1 != expr.nodes.length) return expr;
287 if ('arguments' != expr.nodes[0].nodeName && 'expression' != expr.nodes[0].nodeName) return expr;
288 return exports.unwrap(expr.nodes[0]);
289};
290
291/**
292 * Coerce JavaScript values to their Stylus equivalents.
293 *
294 * @param {Mixed} val
295 * @param {Boolean} [raw]
296 * @return {Node}
297 * @api public
298 */
299
300exports.coerce = function(val, raw){
301 switch (typeof val) {
302 case 'function':
303 return val;
304 case 'string':
305 return new nodes.String(val);
306 case 'boolean':
307 return new nodes.Boolean(val);
308 case 'number':
309 return new nodes.Unit(val);
310 default:
311 if (null == val) return nodes.null;
312 if (Array.isArray(val)) return exports.coerceArray(val, raw);
313 if (val.nodeName) return val;
314 return exports.coerceObject(val, raw);
315 }
316};
317
318/**
319 * Coerce a javascript `Array` to a Stylus `Expression`.
320 *
321 * @param {Array} val
322 * @param {Boolean} [raw]
323 * @return {Expression}
324 * @api private
325 */
326
327exports.coerceArray = function(val, raw){
328 var expr = new nodes.Expression;
329 val.forEach(function(val){
330 expr.push(exports.coerce(val, raw));
331 });
332 return expr;
333};
334
335/**
336 * Coerce a javascript object to a Stylus `Expression` or `Object`.
337 *
338 * For example `{ foo: 'bar', bar: 'baz' }` would become
339 * the expression `(foo 'bar') (bar 'baz')`. If `raw` is true
340 * given `obj` would become a Stylus hash object.
341 *
342 * @param {Object} obj
343 * @param {Boolean} [raw]
344 * @return {Expression|Object}
345 * @api public
346 */
347
348exports.coerceObject = function(obj, raw){
349 var node = raw ? new nodes.Object : new nodes.Expression
350 , val;
351
352 for (var key in obj) {
353 val = exports.coerce(obj[key], raw);
354 key = new nodes.Ident(key);
355 if (raw) {
356 node.set(key, val);
357 } else {
358 node.push(exports.coerceArray([key, val]));
359 }
360 }
361
362 return node;
363};
364
365/**
366 * Return param names for `fn`.
367 *
368 * @param {Function} fn
369 * @return {Array}
370 * @api private
371 */
372
373exports.params = function(fn){
374 return fn
375 .toString()
376 .match(/\(([^)]*)\)/)[1].split(/ *, */);
377};
378
379/**
380 * Merge object `b` with `a`.
381 *
382 * @param {Object} a
383 * @param {Object} b
384 * @return {Object} a
385 * @api private
386 */
387
388exports.merge = function(a, b){
389 for (var k in b) a[k] = b[k];
390 return a;
391};
392
393/**
394 * Returns an array with unique values.
395 *
396 * @param {Array} arr
397 * @return {Array}
398 * @api private
399 */
400
401exports.uniq = function(arr){
402 var obj = {}
403 , ret = [];
404
405 for (var i = 0, len = arr.length; i < len; ++i) {
406 if (arr[i] in obj) continue;
407
408 obj[arr[i]] = true;
409 ret.push(arr[i]);
410 }
411 return ret;
412};
413
414/**
415 * Compile selector strings in `arr` from the bottom-up
416 * to produce the selector combinations. For example
417 * the following Stylus:
418 *
419 * ul
420 * li
421 * p
422 * a
423 * color: red
424 *
425 * Would return:
426 *
427 * [ 'ul li a', 'ul p a' ]
428 *
429 * @param {Array} arr
430 * @param {Boolean} leaveHidden
431 * @return {Array}
432 * @api private
433 */
434
435var HIDDEN_SELECTOR_RX = /^\s*\/?\$/
436 , ROOT_SELECTOR_RX = /^\//g
437 , PARENT_SELECTOR_RX = /^&|([^\\])&/g
438 , ESCAPED_PARENT_SELECTOR_RX = /\\&/g;
439
440exports.compileSelectors = function(arr, leaveHidden){
441 var self = this
442 , selectors = []
443 , buf = [];
444
445 function interpolateParent(selector, buf) {
446 var str = selector.val.replace(ROOT_SELECTOR_RX, '').trim();
447 if (buf.length) {
448 for (var i = 0, len = buf.length; i < len; ++i) {
449 if (~buf[i].indexOf('&') || '/' === buf[i].charAt(0)) {
450 str = buf[i].replace(PARENT_SELECTOR_RX, '$1' + str).replace(ROOT_SELECTOR_RX, '').trim();
451 } else {
452 str += ' ' + buf[i].trim();
453 }
454 }
455 }
456 return str.trim();
457 }
458
459 function compile(arr, i) {
460 if (i) {
461 arr[i].forEach(function(selector){
462 if (!leaveHidden && selector.val.match(HIDDEN_SELECTOR_RX)) return;
463 if (selector.inherits) {
464 buf.unshift(selector.val);
465 compile(arr, i - 1);
466 buf.shift();
467 } else {
468 selectors.push(interpolateParent(selector, buf));
469 }
470 });
471 } else {
472 arr[0].forEach(function(selector){
473 if (!leaveHidden && selector.val.match(HIDDEN_SELECTOR_RX)) return;
474 var str = interpolateParent(selector, buf);
475 if (~str.indexOf('&')) str = str.replace(PARENT_SELECTOR_RX, '$1').replace(ESCAPED_PARENT_SELECTOR_RX, '&').trim();
476 if (!str.length) return;
477 selectors.push((self.indent || '') + str.trimRight());
478 });
479 }
480 }
481
482 compile(arr, arr.length - 1);
483
484 // Return the list with unique selectors only
485 return exports.uniq(selectors);
486};