/*! * Stylus - Compiler * Copyright(c) 2010 LearnBoost * MIT Licensed */ /** * Module dependencies. */ var Visitor = require('./') , nodes = require('../nodes') , utils = require('../utils') , fs = require('fs'); /** * Initialize a new `Compiler` with the given `root` Node * and the following `options`. * * Options: * * - `compress` Compress the CSS output (default: false) * * @param {Node} root * @api public */ var Compiler = module.exports = function Compiler(root, options) { options = options || {}; this.compress = options.compress; this.firebug = options.firebug; this.linenos = options.linenos; this.spaces = options['indent spaces'] || 2; this.includeCSS = options['include css']; this.indents = 1; Visitor.call(this, root); this.stack = []; this.js = ''; }; /** * Inherit from `Visitor.prototype`. */ Compiler.prototype.__proto__ = Visitor.prototype; /** * Compile to css, and return a string of CSS. * * @return {String} * @api private */ Compiler.prototype.compile = function(){ return this.visit(this.root); }; /** * Return indentation string. * * @return {String} * @api private */ Compiler.prototype.__defineGetter__('indent', function(){ if (this.compress) return ''; return new Array(this.indents).join(Array(this.spaces + 1).join(' ')); }); /** * Visit Root. */ Compiler.prototype.visitRoot = function(block){ this.buf = ''; for (var i = 0, len = block.nodes.length; i < len; ++i) { var node = block.nodes[i]; if (this.linenos || this.firebug) this.debugInfo(node); var ret = this.visit(node); if (ret) this.buf += ret + '\n'; } return this.buf; }; /** * Visit Block. */ Compiler.prototype.visitBlock = function(block){ var node; if (block.hasProperties && !block.lacksRenderedSelectors) { var arr = [this.compress ? '{' : ' {']; ++this.indents; for (var i = 0, len = block.nodes.length; i < len; ++i) { this.last = len - 1 == i; node = block.nodes[i]; switch (node.nodeName) { case 'null': case 'expression': case 'function': case 'jsliteral': case 'group': case 'unit': continue; case 'media': // Prevent double-writing the @media declaration when // nested inside of a function/mixin if (node.block.parent.scope) { continue; } default: arr.push(this.visit(node)); } } --this.indents; arr.push(this.indent + '}'); this.buf += arr.join(this.compress ? '' : '\n'); this.buf += '\n'; } // Nesting for (var i = 0, len = block.nodes.length; i < len; ++i) { node = block.nodes[i]; switch (node.nodeName) { case 'group': case 'print': case 'page': case 'block': case 'keyframes': if (this.linenos || this.firebug) this.debugInfo(node); this.visit(node); break; case 'media': case 'mozdocument': case 'import': case 'fontface': this.visit(node); break; case 'comment': // only show comments inside when outside of scope and unsuppressed if (!block.scope && !node.suppress) { this.buf += this.visit(node) + '\n'; } break; case 'literal': this.buf += this.visit(node) + '\n'; break; } } }; /** * Visit Keyframes. */ Compiler.prototype.visitKeyframes = function(node){ var comma = this.compress ? ',' : ', '; var prefix = 'official' == node.prefix ? '' : '-' + node.prefix + '-'; this.buf += '@' + prefix + 'keyframes ' + this.visit(node.name) + (this.compress ? '{' : ' {'); ++this.indents; node.frames.forEach(function(frame){ if (!this.compress) this.buf += '\n '; this.buf += this.visit(frame.pos.join(comma)); this.visit(frame.block); }, this); --this.indents; this.buf += '}' + (this.compress ? '' : '\n'); }; /** * Visit Media. */ Compiler.prototype.visitMedia = function(media){ this.buf += '@media ' + media.val; this.buf += this.compress ? '{' : ' {\n'; ++this.indents; this.visit(media.block); --this.indents; this.buf += '}' + (this.compress ? '' : '\n'); }; /** * Visit MozDocument. */ Compiler.prototype.visitMozDocument = function(mozdocument){ this.buf += '@-moz-document ' + mozdocument.val; this.buf += this.compress ? '{' : ' {\n'; ++this.indents; this.visit(mozdocument.block); --this.indents; this.buf += '}' + (this.compress ? '' : '\n'); }; /** * Visit Page. */ Compiler.prototype.visitPage = function(page){ this.buf += this.indent + '@page'; this.buf += page.selector ? ' ' + page.selector : ''; this.visit(page.block); }; /** * Visit Import. */ Compiler.prototype.visitImport = function(imported){ this.buf += '@import ' + this.visit(imported.path) + ';\n'; }; /** * Visit FontFace. */ Compiler.prototype.visitFontFace = function(face){ this.buf += this.indent + '@font-face'; this.visit(face.block); }; /** * Visit JSLiteral. */ Compiler.prototype.visitJSLiteral = function(js){ this.js += '\n' + js.val.replace(/@selector/g, '"' + this.selector + '"'); return ''; }; /** * Visit Comment. */ Compiler.prototype.visitComment = function(comment){ return this.compress ? comment.suppress ? '' : comment.str : comment.str; }; /** * Visit Function. */ Compiler.prototype.visitFunction = function(fn){ return fn.name; }; /** * Visit Variable. */ Compiler.prototype.visitVariable = function(variable){ return ''; }; /** * Visit Charset. */ Compiler.prototype.visitCharset = function(charset){ return '@charset ' + this.visit(charset.val) + ';'; }; /** * Visit Literal. */ Compiler.prototype.visitLiteral = function(lit){ var val = lit.val.trim(); if (!this.includeCSS) val = val.replace(/^ /gm, ''); return val; }; /** * Visit Boolean. */ Compiler.prototype.visitBoolean = function(bool){ return bool.toString(); }; /** * Visit RGBA. */ Compiler.prototype.visitRGBA = function(rgba){ return rgba.toString(); }; /** * Visit HSLA. */ Compiler.prototype.visitHSLA = function(hsla){ return hsla.rgba.toString(); }; /** * Visit Unit. */ Compiler.prototype.visitUnit = function(unit){ var type = unit.type || '' , n = unit.val , float = n != (n | 0); // Compress if (this.compress) { // Zero is always '0', unless when // a percentage, this is required by keyframes if ('%' != type && 0 == n) return '0'; // Omit leading '0' on floats if (float && n < 1 && n > -1) { return n.toString().replace('0.', '.') + type; } } return n.toString() + type; }; /** * Visit Group. */ Compiler.prototype.visitGroup = function(group){ var stack = this.stack; stack.push(group.nodes); // selectors if (group.block.hasProperties) { var selectors = this.compileSelectors(stack); if(selectors.length) this.buf += (this.selector = selectors.join(this.compress ? ',' : ',\n')); else group.block.lacksRenderedSelectors = true; } // output block this.visit(group.block); stack.pop(); }; /** * Visit Ident. */ Compiler.prototype.visitIdent = function(ident){ return ident.name; }; /** * Visit String. */ Compiler.prototype.visitString = function(string){ return this.isURL ? string.val : string.toString(); }; /** * Visit Null. */ Compiler.prototype.visitNull = function(node){ return ''; }; /** * Visit Call. */ Compiler.prototype.visitCall = function(call){ this.isURL = 'url' == call.name; var args = call.args.nodes.map(function(arg){ return this.visit(arg); }, this).join(this.compress ? ',' : ', '); if (this.isURL) args = '"' + args + '"'; this.isURL = false; return call.name + '(' + args + ')'; }; /** * Visit Expression. */ Compiler.prototype.visitExpression = function(expr){ var buf = [] , self = this , len = expr.nodes.length , nodes = expr.nodes.map(function(node){ return self.visit(node); }); nodes.forEach(function(node, i){ var last = i == len - 1; buf.push(node); if ('/' == nodes[i + 1] || '/' == node) return; if (last) return; buf.push(expr.isList ? (self.compress ? ',' : ', ') : (self.isURL ? '' : ' ')); }); return buf.join(''); }; /** * Visit Arguments. */ Compiler.prototype.visitArguments = Compiler.prototype.visitExpression; /** * Visit Property. */ Compiler.prototype.visitProperty = function(prop){ var self = this , val = this.visit(prop.expr).trim(); return this.indent + (prop.name || prop.segments.join('')) + (this.compress ? ':' + val : ': ' + val) + (this.compress ? (this.last ? '' : ';') : ';'); }; /** * 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 * @return {Array} * @api private */ Compiler.prototype.compileSelectors = function(arr){ var stack = this.stack , self = this , selectors = [] , buf = [] , hiddenSelectorRegexp = /^\s*\$/; function interpolateParent(selector, buf) { var str = selector.val.trim(); if (buf.length) { for (var i = 0, len = buf.length; i < len; ++i) { if (~buf[i].indexOf('&')) { str = buf[i].replace(/&/g, str).trim(); } else { str += ' ' + buf[i].trim(); } } } return str; } function compile(arr, i) { if (i) { arr[i].forEach(function(selector){ if(selector.val.match(hiddenSelectorRegexp)) return; if (selector.inherits) { buf.unshift(selector.val); compile(arr, i - 1); buf.shift(); } else { selectors.push(interpolateParent(selector, buf)); } }); } else { arr[0].forEach(function(selector){ if(selector.val.match(hiddenSelectorRegexp)) return; var str = interpolateParent(selector, buf); selectors.push(self.indent + str.trimRight()); }); } } compile(arr, arr.length - 1); return selectors; }; /** * Debug info. */ Compiler.prototype.debugInfo = function(node){ var path = node.filename == 'stdin' ? 'stdin' : fs.realpathSync(node.filename) , line = node.nodes ? node.nodes[0].lineno : node.lineno; if (this.linenos){ this.buf += '\n/* ' + 'line ' + line + ' : ' + path + ' */\n'; } if (this.firebug){ // debug info for firebug, the crazy formatting is needed path = 'file\\\:\\\/\\\/' + path.replace(/(\/|\.)/g, '\\$1'); line = '\\00003' + line; this.buf += '\n@media -stylus-debug-info' + '{filename{font-family:' + path + '}line{font-family:' + line + '}}\n'; } }