UNPKG

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