You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
526 lines
12 KiB
526 lines
12 KiB
|
|
/*! |
|
* Stylus - utils |
|
* Copyright (c) Automattic <developer.wordpress.com> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var nodes = require('./nodes') |
|
, basename = require('path').basename |
|
, relative = require('path').relative |
|
, join = require('path').join |
|
, isAbsolute = require('path').isAbsolute |
|
, glob = require('glob') |
|
, fs = require('fs'); |
|
|
|
/** |
|
* Check if `path` looks absolute. |
|
* |
|
* @param {String} path |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
exports.absolute = isAbsolute || function(path){ |
|
// On Windows the path could start with a drive letter, i.e. a:\\ or two leading backslashes. |
|
// Also on Windows, the path may have been normalized to forward slashes, so check for this too. |
|
return path.substr(0, 2) == '\\\\' || '/' === path.charAt(0) || /^[a-z]:[\\\/]/i.test(path); |
|
}; |
|
|
|
/** |
|
* Attempt to lookup `path` within `paths` from tail to head. |
|
* Optionally a path to `ignore` may be passed. |
|
* |
|
* @param {String} path |
|
* @param {String} paths |
|
* @param {String} ignore |
|
* @return {String} |
|
* @api private |
|
*/ |
|
|
|
exports.lookup = function(path, paths, ignore){ |
|
var lookup |
|
, i = paths.length; |
|
|
|
// Absolute |
|
if (exports.absolute(path)) { |
|
try { |
|
fs.statSync(path); |
|
return path; |
|
} catch (err) { |
|
// Ignore, continue on |
|
// to trying relative lookup. |
|
// Needed for url(/images/foo.png) |
|
// for example |
|
} |
|
} |
|
|
|
// Relative |
|
while (i--) { |
|
try { |
|
lookup = join(paths[i], path); |
|
if (ignore == lookup) continue; |
|
fs.statSync(lookup); |
|
return lookup; |
|
} catch (err) { |
|
// Ignore |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* Like `utils.lookup` but uses `glob` to find files. |
|
* |
|
* @param {String} path |
|
* @param {String} paths |
|
* @param {String} ignore |
|
* @return {Array} |
|
* @api private |
|
*/ |
|
exports.find = function(path, paths, ignore) { |
|
var lookup |
|
, found |
|
, i = paths.length; |
|
|
|
// Absolute |
|
if (exports.absolute(path)) { |
|
if ((found = glob.sync(path)).length) { |
|
return found; |
|
} |
|
} |
|
|
|
// Relative |
|
while (i--) { |
|
lookup = join(paths[i], path); |
|
if (ignore == lookup) continue; |
|
if ((found = glob.sync(lookup)).length) { |
|
return found; |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* Lookup index file inside dir with given `name`. |
|
* |
|
* @param {String} name |
|
* @return {Array} |
|
* @api private |
|
*/ |
|
|
|
exports.lookupIndex = function(name, paths, filename){ |
|
// foo/index.styl |
|
var found = exports.find(join(name, 'index.styl'), paths, filename); |
|
if (!found) { |
|
// foo/foo.styl |
|
found = exports.find(join(name, basename(name).replace(/\.styl/i, '') + '.styl'), paths, filename); |
|
} |
|
if (!found && !~name.indexOf('node_modules')) { |
|
// node_modules/foo/.. or node_modules/foo.styl/.. |
|
found = lookupPackage(join('node_modules', name)); |
|
} |
|
return found; |
|
|
|
function lookupPackage(dir) { |
|
var pkg = exports.lookup(join(dir, 'package.json'), paths, filename); |
|
if (!pkg) { |
|
return /\.styl$/i.test(dir) ? exports.lookupIndex(dir, paths, filename) : lookupPackage(dir + '.styl'); |
|
} |
|
var main = require(relative(__dirname, pkg)).main; |
|
if (main) { |
|
found = exports.find(join(dir, main), paths, filename); |
|
} else { |
|
found = exports.lookupIndex(dir, paths, filename); |
|
} |
|
return found; |
|
} |
|
}; |
|
|
|
/** |
|
* Format the given `err` with the given `options`. |
|
* |
|
* Options: |
|
* |
|
* - `filename` context filename |
|
* - `context` context line count [8] |
|
* - `lineno` context line number |
|
* - `column` context column number |
|
* - `input` input string |
|
* |
|
* @param {Error} err |
|
* @param {Object} options |
|
* @return {Error} |
|
* @api private |
|
*/ |
|
|
|
exports.formatException = function(err, options){ |
|
var lineno = options.lineno |
|
, column = options.column |
|
, filename = options.filename |
|
, str = options.input |
|
, context = options.context || 8 |
|
, context = context / 2 |
|
, lines = ('\n' + str).split('\n') |
|
, start = Math.max(lineno - context, 1) |
|
, end = Math.min(lines.length, lineno + context) |
|
, pad = end.toString().length; |
|
|
|
var context = lines.slice(start, end).map(function(line, i){ |
|
var curr = i + start; |
|
return ' ' |
|
+ Array(pad - curr.toString().length + 1).join(' ') |
|
+ curr |
|
+ '| ' |
|
+ line |
|
+ (curr == lineno |
|
? '\n' + Array(curr.toString().length + 5 + column).join('-') + '^' |
|
: ''); |
|
}).join('\n'); |
|
|
|
err.message = filename |
|
+ ':' + lineno |
|
+ ':' + column |
|
+ '\n' + context |
|
+ '\n\n' + err.message + '\n' |
|
+ (err.stylusStack ? err.stylusStack + '\n' : ''); |
|
|
|
// Don't show JS stack trace for Stylus errors |
|
if (err.fromStylus) err.stack = 'Error: ' + err.message; |
|
|
|
return err; |
|
}; |
|
|
|
/** |
|
* Assert that `node` is of the given `type`, or throw. |
|
* |
|
* @param {Node} node |
|
* @param {Function} type |
|
* @param {String} param |
|
* @api public |
|
*/ |
|
|
|
exports.assertType = function(node, type, param){ |
|
exports.assertPresent(node, param); |
|
if (node.nodeName == type) return; |
|
var actual = node.nodeName |
|
, msg = 'expected ' |
|
+ (param ? '"' + param + '" to be a ' : '') |
|
+ type + ', but got ' |
|
+ actual + ':' + node; |
|
throw new Error('TypeError: ' + msg); |
|
}; |
|
|
|
/** |
|
* Assert that `node` is a `String` or `Ident`. |
|
* |
|
* @param {Node} node |
|
* @param {String} param |
|
* @api public |
|
*/ |
|
|
|
exports.assertString = function(node, param){ |
|
exports.assertPresent(node, param); |
|
switch (node.nodeName) { |
|
case 'string': |
|
case 'ident': |
|
case 'literal': |
|
return; |
|
default: |
|
var actual = node.nodeName |
|
, msg = 'expected string, ident or literal, but got ' + actual + ':' + node; |
|
throw new Error('TypeError: ' + msg); |
|
} |
|
}; |
|
|
|
/** |
|
* Assert that `node` is a `RGBA` or `HSLA`. |
|
* |
|
* @param {Node} node |
|
* @param {String} param |
|
* @api public |
|
*/ |
|
|
|
exports.assertColor = function(node, param){ |
|
exports.assertPresent(node, param); |
|
switch (node.nodeName) { |
|
case 'rgba': |
|
case 'hsla': |
|
return; |
|
default: |
|
var actual = node.nodeName |
|
, msg = 'expected rgba or hsla, but got ' + actual + ':' + node; |
|
throw new Error('TypeError: ' + msg); |
|
} |
|
}; |
|
|
|
/** |
|
* Assert that param `name` is given, aka the `node` is passed. |
|
* |
|
* @param {Node} node |
|
* @param {String} name |
|
* @api public |
|
*/ |
|
|
|
exports.assertPresent = function(node, name){ |
|
if (node) return; |
|
if (name) throw new Error('"' + name + '" argument required'); |
|
throw new Error('argument missing'); |
|
}; |
|
|
|
/** |
|
* Unwrap `expr`. |
|
* |
|
* Takes an expressions with length of 1 |
|
* such as `((1 2 3))` and unwraps it to `(1 2 3)`. |
|
* |
|
* @param {Expression} expr |
|
* @return {Node} |
|
* @api public |
|
*/ |
|
|
|
exports.unwrap = function(expr){ |
|
// explicitly preserve the expression |
|
if (expr.preserve) return expr; |
|
if ('arguments' != expr.nodeName && 'expression' != expr.nodeName) return expr; |
|
if (1 != expr.nodes.length) return expr; |
|
if ('arguments' != expr.nodes[0].nodeName && 'expression' != expr.nodes[0].nodeName) return expr; |
|
return exports.unwrap(expr.nodes[0]); |
|
}; |
|
|
|
/** |
|
* Coerce JavaScript values to their Stylus equivalents. |
|
* |
|
* @param {Mixed} val |
|
* @param {Boolean} [raw] |
|
* @return {Node} |
|
* @api public |
|
*/ |
|
|
|
exports.coerce = function(val, raw){ |
|
switch (typeof val) { |
|
case 'function': |
|
return val; |
|
case 'string': |
|
return new nodes.String(val); |
|
case 'boolean': |
|
return new nodes.Boolean(val); |
|
case 'number': |
|
return new nodes.Unit(val); |
|
default: |
|
if (null == val) return nodes.null; |
|
if (Array.isArray(val)) return exports.coerceArray(val, raw); |
|
if (val.nodeName) return val; |
|
return exports.coerceObject(val, raw); |
|
} |
|
}; |
|
|
|
/** |
|
* Coerce a javascript `Array` to a Stylus `Expression`. |
|
* |
|
* @param {Array} val |
|
* @param {Boolean} [raw] |
|
* @return {Expression} |
|
* @api private |
|
*/ |
|
|
|
exports.coerceArray = function(val, raw){ |
|
var expr = new nodes.Expression; |
|
val.forEach(function(val){ |
|
expr.push(exports.coerce(val, raw)); |
|
}); |
|
return expr; |
|
}; |
|
|
|
/** |
|
* Coerce a javascript object to a Stylus `Expression` or `Object`. |
|
* |
|
* For example `{ foo: 'bar', bar: 'baz' }` would become |
|
* the expression `(foo 'bar') (bar 'baz')`. If `raw` is true |
|
* given `obj` would become a Stylus hash object. |
|
* |
|
* @param {Object} obj |
|
* @param {Boolean} [raw] |
|
* @return {Expression|Object} |
|
* @api public |
|
*/ |
|
|
|
exports.coerceObject = function(obj, raw){ |
|
var node = raw ? new nodes.Object : new nodes.Expression |
|
, val; |
|
|
|
for (var key in obj) { |
|
val = exports.coerce(obj[key], raw); |
|
key = new nodes.Ident(key); |
|
if (raw) { |
|
node.set(key, val); |
|
} else { |
|
node.push(exports.coerceArray([key, val])); |
|
} |
|
} |
|
|
|
return node; |
|
}; |
|
|
|
/** |
|
* Return param names for `fn`. |
|
* |
|
* @param {Function} fn |
|
* @return {Array} |
|
* @api private |
|
*/ |
|
|
|
exports.params = function(fn){ |
|
return fn |
|
.toString() |
|
.match(/\(([^)]*)\)/)[1].split(/ *, */); |
|
}; |
|
|
|
/** |
|
* Merge object `b` with `a`. |
|
* |
|
* @param {Object} a |
|
* @param {Object} b |
|
* @param {Boolean} [deep] |
|
* @return {Object} a |
|
* @api private |
|
*/ |
|
exports.merge = function(a, b, deep) { |
|
for (var k in b) { |
|
if (deep && a[k]) { |
|
var nodeA = exports.unwrap(a[k]).first |
|
, nodeB = exports.unwrap(b[k]).first; |
|
|
|
if ('object' == nodeA.nodeName && 'object' == nodeB.nodeName) { |
|
a[k].first.vals = exports.merge(nodeA.vals, nodeB.vals, deep); |
|
} else { |
|
a[k] = b[k]; |
|
} |
|
} else { |
|
a[k] = b[k]; |
|
} |
|
} |
|
return a; |
|
}; |
|
|
|
/** |
|
* Returns an array with unique values. |
|
* |
|
* @param {Array} arr |
|
* @return {Array} |
|
* @api private |
|
*/ |
|
|
|
exports.uniq = function(arr){ |
|
var obj = {} |
|
, ret = []; |
|
|
|
for (var i = 0, len = arr.length; i < len; ++i) { |
|
if (arr[i] in obj) continue; |
|
|
|
obj[arr[i]] = true; |
|
ret.push(arr[i]); |
|
} |
|
return ret; |
|
}; |
|
|
|
/** |
|
* Compile selector strings in `arr` from the bottom-up |
|
* to produce the selector combinations. For example |
|
* the following Stylus: |
|
* |
|
* ul |
|
* li |
|
* p |
|
* a |
|
* color: red |
|
* |
|
* Would return: |
|
* |
|
* [ 'ul li a', 'ul p a' ] |
|
* |
|
* @param {Array} arr |
|
* @param {Boolean} leaveHidden |
|
* @return {Array} |
|
* @api private |
|
*/ |
|
|
|
exports.compileSelectors = function(arr, leaveHidden){ |
|
var selectors = [] |
|
, Parser = require('./selector-parser') |
|
, indent = (this.indent || '') |
|
, buf = []; |
|
|
|
function parse(selector, buf) { |
|
var parts = [selector.val] |
|
, str = new Parser(parts[0], parents, parts).parse().val |
|
, parents = []; |
|
|
|
if (buf.length) { |
|
for (var i = 0, len = buf.length; i < len; ++i) { |
|
parts.push(buf[i]); |
|
parents.push(str); |
|
child = new Parser(buf[i], parents, parts).parse(); |
|
|
|
if (child.nested) { |
|
str += ' ' + child.val; |
|
} else { |
|
str = child.val; |
|
} |
|
} |
|
} |
|
return str.trim(); |
|
} |
|
|
|
function compile(arr, i) { |
|
if (i) { |
|
arr[i].forEach(function(selector){ |
|
if (!leaveHidden && selector.isPlaceholder) return; |
|
if (selector.inherits) { |
|
buf.unshift(selector.val); |
|
compile(arr, i - 1); |
|
buf.shift(); |
|
} else { |
|
selectors.push(indent + parse(selector, buf)); |
|
} |
|
}); |
|
} else { |
|
arr[0].forEach(function(selector){ |
|
if (!leaveHidden && selector.isPlaceholder) return; |
|
var str = parse(selector, buf); |
|
if (str) selectors.push(indent + str); |
|
}); |
|
} |
|
} |
|
|
|
compile(arr, arr.length - 1); |
|
|
|
// Return the list with unique selectors only |
|
return exports.uniq(selectors); |
|
}; |
|
|
|
/** |
|
* Attempt to parse string. |
|
* |
|
* @param {String} str |
|
* @return {Node} |
|
* @api private |
|
*/ |
|
|
|
exports.parseString = function(str){ |
|
var Parser = require('./parser') |
|
, parser |
|
, ret; |
|
|
|
try { |
|
parser = new Parser(str); |
|
parser.state.push('expression'); |
|
ret = new nodes.Expression(); |
|
ret.nodes = parser.parse().nodes; |
|
} catch (e) { |
|
ret = new nodes.Literal(str); |
|
} |
|
return ret; |
|
};
|
|
|