/*! * Stylus - Evaluator - built-in functions * Copyright(c) 2010 LearnBoost * MIT Licensed */ /** * Module dependencies. */ var Compiler = require('../visitor/compiler') , nodes = require('../nodes') , utils = require('../utils') , Image = require('./image') , units = require('../units') , colors = require('../colors') , path = require('path') , fs = require('fs'); /** * Color component name map. */ var componentMap = { red: 'r' , green: 'g' , blue: 'b' , alpha: 'a' , hue: 'h' , saturation: 's' , lightness: 'l' }; /** * Color component unit type map. */ var unitMap = { hue: 'deg' , saturation: '%' , lightness: '%' }; /** * Color type map. */ var typeMap = { red: 'rgba' , blue: 'rgba' , green: 'rgba' , alpha: 'rgba' , hue: 'hsla' , saturation: 'hsla' , lightness: 'hsla' }; /** * Convert the given `color` to an `HSLA` node, * or h,s,l,a component values. * * Examples: * * hsla(10deg, 50%, 30%, 0.5) * // => HSLA * * hsla(#ffcc00) * // => HSLA * * @param {RGBA|HSLA|Unit} hue * @param {Unit} saturation * @param {Unit} lightness * @param {Unit} alpha * @return {HSLA} * @api public */ exports.hsla = function hsla(hue, saturation, lightness, alpha){ switch (arguments.length) { case 1: utils.assertColor(hue); return hue.hsla; default: utils.assertType(hue, 'unit', 'hue'); utils.assertType(saturation, 'unit', 'saturation'); utils.assertType(lightness, 'unit', 'lightness'); utils.assertType(alpha, 'unit', 'alpha'); if (alpha && '%' == alpha.type) alpha.val /= 100; return new nodes.HSLA( hue.val , saturation.val , lightness.val , alpha.val); } }; /** * Convert the given `color` to an `HSLA` node, * or h,s,l component values. * * Examples: * * hsl(10, 50, 30) * // => HSLA * * hsl(#ffcc00) * // => HSLA * * @param {Unit|HSLA|RGBA} hue * @param {Unit} saturation * @param {Unit} lightness * @return {HSLA} * @api public */ exports.hsl = function hsl(hue, saturation, lightness){ if (1 == arguments.length) { utils.assertColor(hue, 'color'); return hue.hsla; } else { return exports.hsla( hue , saturation , lightness , new nodes.Unit(1)); } }; /** * Return type of `node`. * * Examples: * * type(12) * // => 'unit' * * type(#fff) * // => 'color' * * type(type) * // => 'function' * * type(unbound) * typeof(unbound) * type-of(unbound) * // => 'ident' * * @param {Node} node * @return {String} * @api public */ exports.type = exports.typeof = exports['type-of'] = function type(node){ utils.assertPresent(node, 'expression'); return node.nodeName; }; /** * Return component `name` for the given `color`. * * @param {RGBA|HSLA} color * @param {String} name * @return {Unit} * @api public */ exports.component = function component(color, name) { utils.assertColor(color, 'color'); utils.assertString(name, 'name'); var name = name.string , unit = unitMap[name] , type = typeMap[name] , name = componentMap[name]; if (!name) throw new Error('invalid color component "' + name + '"'); return new nodes.Unit(color[type][name], unit); }; /** * Return the basename of `path`. * * @param {String} path * @return {String} * @api public */ exports.basename = function basename(p, ext){ utils.assertString(p, 'path'); return path.basename(p.val, ext && ext.val); }; /** * Return the dirname of `path`. * * @param {String} path * @return {String} * @api public */ exports.dirname = function dirname(p){ utils.assertString(p, 'path'); return path.dirname(p.val); }; /** * Return the extname of `path`. * * @param {String} path * @return {String} * @api public */ exports.extname = function extname(p){ utils.assertString(p, 'path'); return path.extname(p.val); }; /** * Peform a path join. * * @param {String} path * @return {String} * @api public */ (exports.pathjoin = function pathjoin(){ var paths = [].slice.call(arguments).map(function(path){ return path.first.string; }); return path.join.apply(null, paths); }).raw = true; /** * Return the red component of the given `color`. * * Examples: * * red(#c00) * // => 204 * * @param {RGBA|HSLA} color * @return {Unit} * @api public */ exports.red = function red(color){ return exports.component(color, new nodes.String('red')); }; /** * Return the green component of the given `color`. * * Examples: * * green(#0c0) * // => 204 * * @param {RGBA|HSLA} color * @return {Unit} * @api public */ exports.green = function green(color){ return exports.component(color, new nodes.String('green')); }; /** * Return the blue component of the given `color`. * * Examples: * * blue(#00c) * // => 204 * * @param {RGBA|HSLA} color * @return {Unit} * @api public */ exports.blue = function blue(color){ return exports.component(color, new nodes.String('blue')); }; /** * Return a `RGBA` from the r,g,b,a channels. * * Examples: * * rgba(255,0,0,0.5) * // => rgba(255,0,0,0.5) * * rgba(255,0,0,1) * // => #ff0000 * * rgba(#ffcc00, 50%) * // rgba(255,204,0,0.5) * * @param {Unit|RGBA|HSLA} red * @param {Unit} green * @param {Unit} blue * @param {Unit} alpha * @return {RGBA} * @api public */ exports.rgba = function rgba(red, green, blue, alpha){ switch (arguments.length) { case 1: utils.assertColor(red); var color = red.rgba; return new nodes.RGBA( color.r , color.g , color.b , color.a); case 2: utils.assertColor(red); var color = red.rgba; utils.assertType(green, 'unit', 'alpha'); if ('%' == green.type) green.val /= 100; return new nodes.RGBA( color.r , color.g , color.b , green.val); default: utils.assertType(red, 'unit', 'red'); utils.assertType(green, 'unit', 'green'); utils.assertType(blue, 'unit', 'blue'); utils.assertType(alpha, 'unit', 'alpha'); var r = '%' == red.type ? Math.round(red.val * 2.55) : red.val; var g = '%' == green.type ? Math.round(green.val * 2.55) : green.val; var b = '%' == blue.type ? Math.round(blue.val * 2.55) : blue.val; if (alpha && '%' == alpha.type) alpha.val /= 100; return new nodes.RGBA( r , g , b , alpha.val); } }; /** * Return a `RGBA` from the r,g,b channels. * * Examples: * * rgb(255,204,0) * // => #ffcc00 * * rgb(#fff) * // => #fff * * @param {Unit|RGBA|HSLA} red * @param {Unit} green * @param {Unit} blue * @return {RGBA} * @api public */ exports.rgb = function rgb(red, green, blue){ switch (arguments.length) { case 1: utils.assertColor(red); var color = red.rgba; return new nodes.RGBA( color.r , color.g , color.b , 1); default: return exports.rgba( red , green , blue , new nodes.Unit(1)); } }; /** * Convert a .json file into stylus variables * Nested variable object keys are joined with a dash (-) * * Given this sample media-queries.json file: * { * "small": "screen and (max-width:400px)", * "tablet": { * "landscape": "screen and (min-width:600px) and (orientation:landscape)", * "portrait": "screen and (min-width:600px) and (orientation:portrait)" * } * } * * Examples: * * json('media-queries.json') * * @media small * // => @media screen and (max-width:400px) * * @media tablet-landscape * // => @media screen and (min-width:600px) and (orientation:landscape) * * @param {String} path * @param {Boolean} [local] * @param {String} [namePrefix] * @api public */ exports.json = function(path, local, namePrefix){ utils.assertString(path, 'path'); if (namePrefix) { utils.assertString(namePrefix, 'namePrefix'); namePrefix = namePrefix.val; } else { namePrefix = ''; } local = local ? local.toBoolean() : new nodes.Boolean(local); var scope = local.isTrue ? this.currentScope : this.global.scope; // lookup path = path.string; var found = utils.lookup(path, this.options.paths, this.options.filename); if (!found) throw new Error('failed to locate .json file ' + path); // read var str = fs.readFileSync(found, 'utf8'); convert.call(this, JSON.parse(str)); return; function convert(obj, prefix){ prefix = prefix ? prefix + '-' : ''; for (var key in obj){ var val = obj[key]; var name = prefix + key; if ('object' == typeof val) { convert.call(this, val, name); } else { val = utils.coerce(val); if ('string' == val.nodeName) val = parseUnit(val.string) || parseColor(val.string) || new nodes.Literal(val.string); scope.add({ name: namePrefix + name, val: val }); } } } }; /** * Use the given `plugin` * * Examples: * * use("plugins/add.js") * * width add(10, 100) * // => width: 110 */ exports.use = function(plugin){ utils.assertString(plugin, 'path'); // lookup plugin = plugin.string; var found = utils.lookup(plugin, this.options.paths, this.options.filename); if (!found) throw new Error('failed to locate plugin file ' + plugin); // use var fn = require(path.resolve(found)); if ('function' != typeof fn) { throw new Error('plugin ' + path + ' does not export a function'); } this.renderer.use(fn(this.options)); } /** * Unquote the given `str`. * * Examples: * * unquote("sans-serif") * // => sans-serif * * unquote(sans-serif) * // => sans-serif * * @param {String|Ident} string * @return {Literal} * @api public */ exports.unquote = function unquote(string){ utils.assertString(string, 'string'); return new nodes.Literal(string.string); }; /** * Assign `type` to the given `unit` or return `unit`'s type. * * @param {Unit} unit * @param {String|Ident} type * @return {Unit} * @api public */ exports.unit = function unit(unit, type){ utils.assertType(unit, 'unit', 'unit'); // Assign if (type) { utils.assertString(type, 'type'); return new nodes.Unit(unit.val, type.string); } else { return unit.type || ''; } }; /** * Lookup variable `name` or return Null. * * @param {String} name * @return {Mixed} * @api public */ exports.lookup = function lookup(name){ utils.assertType(name, 'string', 'name'); var node = this.lookup(name.val); if (!node) return nodes.null; return this.visit(node); }; /** * Perform `op` on the `left` and `right` operands. * * @param {String} op * @param {Node} left * @param {Node} right * @return {Node} * @api public */ exports.operate = function operate(op, left, right){ utils.assertType(op, 'string', 'op'); utils.assertPresent(left, 'left'); utils.assertPresent(right, 'right'); return left.operate(op.val, right); }; /** * Test if `val` matches the given `pattern`. * * Examples: * * match('^foo(bar)?', foo) * match('^foo(bar)?', foobar) * match('^foo(bar)?', 'foo') * match('^foo(bar)?', 'foobar') * // => true * * match('^foo(bar)?', 'bar') * // => false * * @param {String} pattern * @param {String|Ident} val * @return {Boolean} * @api public */ exports.match = function match(pattern, val){ utils.assertType(pattern, 'string', 'pattern'); utils.assertString(val, 'val'); var re = new RegExp(pattern.val); return nodes.Boolean(re.test(val.string)); }; /** * Returns substring of the given `val`. * * @param {String|Ident} val * @param {Number} start * @param {Number} [length] * @return {String|Ident} * @api public */ (exports.substr = function substr(val, start, length){ utils.assertPresent(val, 'string'); utils.assertPresent(start, 'start'); var valNode = utils.unwrap(val).nodes[0]; start = utils.unwrap(start).nodes[0].val; if (length) { length = utils.unwrap(length).nodes[0].val; } var res = valNode.string.substr(start, length); return valNode instanceof nodes.Ident ? new nodes.Ident(res) : new nodes.String(res); }).raw = true; /** * Returns string with all matches of `pattern` replaced by `replacement` in given `val` * * @param {String} pattern * @param {String} replacement * @param {String|Ident} val * @return {String|Ident} * @api public */ (exports.replace = function replace(pattern, replacement, val){ utils.assertPresent(pattern, 'pattern'); utils.assertPresent(replacement, 'replacement'); utils.assertPresent(val, 'val'); pattern = new RegExp(utils.unwrap(pattern).nodes[0].string, 'g'); replacement = utils.unwrap(replacement).nodes[0].string; var valNode = utils.unwrap(val).nodes[0]; var res = valNode.string.replace(pattern, replacement); return valNode instanceof nodes.Ident ? new nodes.Ident(res) : new nodes.String(res); }).raw = true; /** * Splits the given `val` by `delim` * * @param {String} delim * @param {String|Ident} val * @return {Expression} * @api public */ (exports.split = function split(delim, val){ utils.assertPresent(delim, 'delimiter'); utils.assertPresent(val, 'val'); delim = utils.unwrap(delim).nodes[0].string; var valNode = utils.unwrap(val).nodes[0]; var splitted = valNode.string.split(delim); var expr = new nodes.Expression(); var ItemNode = valNode instanceof nodes.Ident ? nodes.Ident : nodes.String; for (var i = 0, len = splitted.length; i < len; ++i) { expr.nodes.push(new ItemNode(splitted[i])); } return expr; }).raw = true; /** * Return length of the given `expr`. * * @param {Expression} expr * @return {Unit} * @api public */ (exports.length = function length(expr){ if (expr) { return expr.nodes ? utils.unwrap(expr).nodes.length : 1; } return 0; }).raw = true; /** * Inspect the given `expr`. * * @param {Expression} expr * @api public */ (exports.p = function p(){ [].slice.call(arguments).forEach(function(expr){ expr = utils.unwrap(expr); if (!expr.nodes.length) return; console.log('\033[90minspect:\033[0m %s', expr.toString().replace(/^\(|\)$/g, '')); }) return nodes.null; }).raw = true; /** * Throw an error with the given `msg`. * * @param {String} msg * @api public */ exports.error = function error(msg){ utils.assertType(msg, 'string', 'msg'); throw new Error(msg.val); }; /** * Warn with the given `msg` prefixed by "Warning: ". * * @param {String} msg * @api public */ exports.warn = function warn(msg){ utils.assertType(msg, 'string', 'msg'); console.warn('Warning: %s', msg.val); return nodes.null; }; /** * Output stack trace. * * @api public */ exports.trace = function trace(){ console.log(this.stack); return nodes.null; }; /** * Push the given args to `expr`. * * @param {Expression} expr * @param {Node} ... * @return {Unit} * @api public */ (exports.push = exports.append = function(expr){ expr = utils.unwrap(expr); for (var i = 1, len = arguments.length; i < len; ++i) { expr.nodes.push(utils.unwrap(arguments[i]).clone()); } return expr.nodes.length; }).raw = true; /** * Pop a value from `expr`. * * @param {Expression} expr * @return {Node} * @api public */ (exports.pop = function pop(expr) { expr = utils.unwrap(expr); return expr.nodes.pop(); }).raw = true; /** * Unshift the given args to `expr`. * * @param {Expression} expr * @param {Node} ... * @return {Unit} * @api public */ (exports.unshift = exports.prepend = function(expr){ expr = utils.unwrap(expr); for (var i = 1, len = arguments.length; i < len; ++i) { expr.nodes.unshift(utils.unwrap(arguments[i])); } return expr.nodes.length; }).raw = true; /** * Shift an element from `expr`. * * @param {Expression} expr * @return {Node} * @api public */ (exports.shift = function(expr){ expr = utils.unwrap(expr); return expr.nodes.shift(); }).raw = true; /** * Return a `Literal` with the given `fmt`, and * variable number of arguments. * * @param {String} fmt * @param {Node} ... * @return {Literal} * @api public */ (exports.s = function s(fmt){ fmt = utils.unwrap(fmt).nodes[0]; utils.assertString(fmt); var self = this , str = fmt.string , args = arguments , i = 1; // format str = str.replace(/%(s|d)/g, function(_, specifier){ var arg = args[i++] || nodes.null; switch (specifier) { case 's': return new Compiler(arg, self.options).compile(); case 'd': arg = utils.unwrap(arg).first; if ('unit' != arg.nodeName) throw new Error('%d requires a unit'); return arg.val; } }); return new nodes.Literal(str); }).raw = true; /** * Return a `Literal` `num` converted to the provided `base`, padded to `width` * with zeroes (default width is 2) * * @param {Number} num * @param {Number} base * @param {Number} width * @return {Literal} * @api public */ (exports['base-convert'] = function(num, base, width) { utils.assertPresent(num, 'number'); utils.assertPresent(base, 'base'); num = utils.unwrap(num).nodes[0].val; base = utils.unwrap(base).nodes[0].val; width = (width && utils.unwrap(width).nodes[0].val) || 2; var result = Number(num).toString(base); while (result.length < width) { result = "0" + result; } return new nodes.Literal(result); }).raw = true; /** * Return the opposites of the given `positions`. * * Examples: * * opposite-position(top left) * // => bottom right * * @param {Expression} positions * @return {Expression} * @api public */ (exports['opposite-position'] = function oppositePosition(positions){ var expr = []; utils.unwrap(positions).nodes.forEach(function(pos, i){ utils.assertString(pos, 'position ' + i); pos = (function(){ switch (pos.string) { case 'top': return 'bottom'; case 'bottom': return 'top'; case 'left': return 'right'; case 'right': return 'left'; case 'center': return 'center'; default: throw new Error('invalid position ' + pos); }})(); expr.push(new nodes.Literal(pos)); }); return expr; }).raw = true; /** * Return the width and height of the given `img` path. * * Examples: * * image-size('foo.png') * // => 200px 100px * * image-size('foo.png')[0] * // => 200px * * image-size('foo.png')[1] * // => 100px * * Can be used to test if the image exists, * using an optional argument set to `true` * (without this argument this function throws error * if there is no such image). * * Example: * * image-size('nosuchimage.png', true)[0] * // => 0 * * @param {String} img * @param {Boolean} ignoreErr * @return {Expression} * @api public */ exports['image-size'] = function imageSize(img, ignoreErr) { utils.assertType(img, 'string', 'img'); try { var img = new Image(this, img.string); } catch (err) { if (ignoreErr) { return [new nodes.Unit(0), new nodes.Unit(0)]; } else { throw err; } } // Read size img.open(); var size = img.size(); img.close(); // Return (w h) var expr = []; expr.push(new nodes.Unit(size[0], 'px')); expr.push(new nodes.Unit(size[1], 'px')); return expr; }; /** * Apply Math `fn` to `n`. * * @param {Unit} n * @param {String} fn * @return {Unit} * @api private */ exports['-math'] = function math(n, fn){ return new nodes.Unit(Math[fn.string](n.val), n.type); }; /** * Get Math `prop`. * * @param {String} prop * @return {Unit} * @api private */ exports['-math-prop'] = function math(prop){ return new nodes.Unit(Math[prop.string]); }; /** * Buffer the given js `str`. * * @param {String} str * @return {JSLiteral} * @api private */ exports.js = function js(str){ utils.assertString(str, 'str'); return new nodes.JSLiteral(str.val); }; /** * Adjust HSL `color` `prop` by `amount`. * * @param {RGBA|HSLA} color * @param {String} prop * @param {Unit} amount * @return {RGBA} * @api private */ exports['-adjust'] = function adjust(color, prop, amount){ var hsl = color.hsla.clone(); prop = { hue: 'h', saturation: 's', lightness: 'l' }[prop.string]; if (!prop) throw new Error('invalid adjustment property'); var val = amount.val; if ('%' == amount.type){ val = 'l' == prop && val > 0 ? (100 - hsl[prop]) * val / 100 : hsl[prop] * (val / 100); } hsl[prop] += val; return hsl.rgba; }; /** * Return a clone of the given `expr`. * * @param {Expression} expr * @return {Node} * @api public */ (exports.clone = function clone(expr){ utils.assertPresent(expr, 'expr'); return expr.clone(); }).raw = true; /** * Add property `name` with the given `expr` * to the mixin-able block. * * @param {String|Ident|Literal} name * @param {Expression} expr * @return {Property} * @api public */ (exports['add-property'] = function addProperty(name, expr){ utils.assertType(name, 'expression', 'name'); name = utils.unwrap(name).first; utils.assertString(name, 'name'); utils.assertType(expr, 'expression', 'expr'); var prop = new nodes.Property([name], expr); var block = this.closestBlock; var len = block.nodes.length , head = block.nodes.slice(0, block.index) , tail = block.nodes.slice(block.index++, len); head.push(prop); block.nodes = head.concat(tail); return prop; }).raw = true; /** * Attempt to parse unit `str`. * * @param {String} str * @return {Unit} * @api public */ function parseUnit(str){ var m = str.match(/^(\d+)(.*)/); if (!m) return; var n = parseInt(m[1], 10); var type = m[2]; return new nodes.Unit(n, type); } /** * Attempt to parse color * @param {String} str * @return {RGBA} * @api public */ function parseColor(str){ if (str.substr(0,1) === '#'){ var m = str.match(/\w{2}/g); if (!m) return; m = m.map(function(s){ return parseInt(s, 16) }); return new nodes.RGBA(m[0],m[1],m[2],1); } else if (str.substr(0,3) === 'rgb'){ var m = str.match(/(\d\.*\d+)/g); if (!m) return; m = m.map(function(s){return parseFloat(s, 10)}); return new nodes.RGBA(m[0], m[1], m[2], m[3] || 1); } else { var rgb = colors[str]; if (!rgb) return; return new nodes.RGBA(rgb[0], rgb[1], rgb[2], 1); } }