hexo/node_modules/stylus/lib/utils.js

525 lines
12 KiB
JavaScript

/*!
* 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);
var 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);
ret = parser.list();
} catch (e) {
ret = new nodes.Literal(str);
}
return ret;
};