/*! * 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 or object. * 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) * * vars = json('vars.json', { hash: true }) * body * width: vars.width * * @param {String} path * @param {Boolean} [local] * @param {String} [namePrefix] * @api public */ exports.json = function(path, local, namePrefix){ utils.assertString(path, 'path'); // 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 json = JSON.parse(fs.readFileSync(found, 'utf8')); if (local && 'object' == local.nodeName) { return convert(json); } else { exports['-old-json'].call(this, json, local, namePrefix); } function convert(obj){ var ret = new nodes.Object(); for (var key in obj) { var val = obj[key]; if ('object' == typeof val) { ret.set(key, convert(val)); } else { val = utils.coerce(val); if ('string' == val.nodeName) val = parseString(val.string); ret.set(key, val); } } return ret; } }; /** * Old `json` BIF. * * @api private */ exports['-old-json'] = function(json, local, namePrefix){ 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; convert(json); 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(val, name); } else { val = utils.coerce(val); if ('string' == val.nodeName) val = parseString(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, options){ utils.assertString(plugin, 'plugin'); if (options) { utils.assertType(options, 'object', 'options'); options = parseObject(options); } // 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 "' + plugin + '" does not export a function'); } this.renderer.use(fn(options || 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); }; /** * Set a variable `name` on current scope. * * @param {String} name * @param {Expression} expr * @api public */ exports.define = function define(name, expr){ utils.assertType(name, 'string', 'name'); expr = utils.unwrap(expr); var scope = this.currentScope; var node = new nodes.Ident(name.val, expr); scope.add(node); return nodes.null; }; /** * 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 new 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) { if (expr.nodes) { var nodes = utils.unwrap(expr).nodes; if (1 == nodes.length && 'object' == nodes[0].nodeName) { return nodes[0].length; } else { return nodes.length; } } else { return 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; }; /** * Return the tangent of the given `angle`. * * @param {Unit} angle * @return {Unit} * @api public */ exports.tan = function tan(angle) { utils.assertType(angle, 'unit', 'angle'); var radians = angle.val; if (angle.type === 'deg') { radians *= Math.PI / 180; } var m = Math.pow(10, 9); var sin = Math.round(Math.sin(radians) * m) / m , cos = Math.round(Math.cos(radians) * m) / m , tan = Math.round(m * sin / cos ) / m; return new nodes.Unit(tan, ''); } /** * Apply Math `fn` to `n`. * * @param {Unit} n * @param {String} fn * @return {Unit} * @api private */ exports.math = function math(n, fn){ utils.assertType(n, 'unit', 'n'); utils.assertString(fn, '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]); }; /** * 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){ utils.assertColor(color, 'color'); utils.assertString(prop, 'prop'); utils.assertType(amount, 'unit', '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; /** * Merge the object `dest` with the given args. * * @param {Object} dest * @param {Object} ... * @return {Object} dest * @api public */ (exports.merge = exports.extend = function merge(dest){ utils.assertPresent(dest, 'dest'); dest = utils.unwrap(dest).first; utils.assertType(dest, 'object', 'dest'); for (var i = 1, len = arguments.length; i < len; ++i) { utils.merge(dest.vals, utils.unwrap(arguments[i]).first.vals); } return dest; }).raw = true; /** * Return the current selector or compile `sel` selector. * * @param {String} [sel] * @return {String} * @api public */ exports.selector = function selector(sel){ var stack = this.selectorStack , group; if (sel && 'string' == sel.nodeName) { if (!~sel.val.indexOf('&') && '/' !== sel.val.charAt(0)) return sel.val; group = new nodes.Group; sel = new nodes.Selector([sel.val]); sel.val = sel.segments.join(''); group.push(sel); stack.push(group.nodes); } return stack.length ? utils.compileSelectors(stack).join(',') : '&'; }; /** * Prefix css classes in a block * * @param {String} prefix * @param {Block} block * @return {Block} * @api private */ exports['-prefix-classes'] = function prefixClasses(prefix, block){ utils.assertString(prefix, 'prefix'); utils.assertType(block, 'block', 'block'); var _prefix = this.prefix; this.options.prefix = this.prefix = prefix.string; block = this.visit(block); this.options.prefix = this.prefix = _prefix; return block; }; /** * Returns the @media string for the current block * * @return {String} * @api public */ exports['current-media'] = function currentMedia(){ return new nodes.String(lookForMedia(this.closestBlock.node) || ''); function lookForMedia(node){ if ('media' == node.nodeName) { return node.toString(); } else if (node.block.parent.node) { return lookForMedia(node.block.parent.node); } } }; /** * Attempt to parse unit `str`. * * @param {String} str * @return {Unit} * @api private */ 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 private */ function parseColor(str){ if (str.substr(0,1) === '#') { // Handle color shorthands (like #abc) var shorthand = str.length === 4, m = str.match(shorthand ? /\w/g : /\w{2}/g); if (!m) return; m = m.map(function(s) { return parseInt(shorthand ? s+s : s, 16) }); return new nodes.RGBA(m[0],m[1],m[2],1); } else if (str.substr(0,3) === 'rgb'){ var m = str.match(/([0-9]*\.?[0-9]+)/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); } } /** * Attempt to parse string. * * @param {String} str * @return {Unit|RGBA|Literal} * @api private */ function parseString(str){ return parseUnit(str) || parseColor(str) || new nodes.Literal(str); } /** * Attempt to parse object node to the javascript object. * * @param {Object} obj * @return {Object} * @api private */ function parseObject(obj){ obj = obj.vals; for (var key in obj) { var nodes = obj[key].nodes[0].nodes; if (nodes && nodes.length) { obj[key] = []; for (var i = 0, len = nodes.length; i < len; ++i) { obj[key].push(convert(nodes[i])); } } else { obj[key] = convert(obj[key].first); } } return obj; function convert(node){ switch (node.nodeName) { case 'object': return parseObject(node); case 'boolean': return node.isTrue; case 'unit': return node.type ? node.toString() : +node.val; case 'string': case 'literal': return node.val; default: return node.toString(); } } }