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.
568 lines
12 KiB
568 lines
12 KiB
/*! |
|
* Stylus - Compiler |
|
* Copyright (c) Automattic <developer.wordpress.com> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var Visitor = require('./') |
|
, 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 = []; |
|
}; |
|
|
|
/** |
|
* 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); |
|
}; |
|
|
|
/** |
|
* Output `str` |
|
* |
|
* @param {String} str |
|
* @param {Node} node |
|
* @return {String} |
|
* @api private |
|
*/ |
|
|
|
Compiler.prototype.out = function(str, node){ |
|
return str; |
|
}; |
|
|
|
/** |
|
* 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(' ')); |
|
}); |
|
|
|
/** |
|
* Check if given `node` needs brackets. |
|
* |
|
* @param {Node} node |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
Compiler.prototype.needBrackets = function(node){ |
|
return 1 == this.indents |
|
|| 'atrule' != node.nodeName |
|
|| node.hasOnlyProperties; |
|
}; |
|
|
|
/** |
|
* 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 += this.out(ret + '\n', node); |
|
} |
|
return this.buf; |
|
}; |
|
|
|
/** |
|
* Visit Block. |
|
*/ |
|
|
|
Compiler.prototype.visitBlock = function(block){ |
|
var node |
|
, separator = this.compress ? '' : '\n' |
|
, needBrackets; |
|
|
|
if (block.hasProperties && !block.lacksRenderedSelectors) { |
|
needBrackets = this.needBrackets(block.node); |
|
|
|
if (needBrackets) { |
|
this.buf += this.out(this.compress ? '{' : ' {\n'); |
|
++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 'group': |
|
case 'block': |
|
case 'unit': |
|
case 'media': |
|
case 'keyframes': |
|
case 'atrule': |
|
case 'supports': |
|
continue; |
|
// inline comments |
|
case !this.compress && node.inline && 'comment': |
|
this.buf = this.buf.slice(0, -1); |
|
this.buf += this.out(' ' + this.visit(node) + '\n', node); |
|
break; |
|
case 'property': |
|
var ret = this.visit(node) + separator; |
|
this.buf += this.compress ? ret : this.out(ret, node); |
|
break; |
|
default: |
|
this.buf += this.out(this.visit(node) + separator, node); |
|
} |
|
} |
|
if (needBrackets) { |
|
--this.indents; |
|
this.buf += this.out(this.indent + '}' + separator); |
|
} |
|
} |
|
|
|
// Nesting |
|
for (var i = 0, len = block.nodes.length; i < len; ++i) { |
|
node = block.nodes[i]; |
|
switch (node.nodeName) { |
|
case 'group': |
|
case 'block': |
|
case 'keyframes': |
|
if (this.linenos || this.firebug) this.debugInfo(node); |
|
this.visit(node); |
|
break; |
|
case 'media': |
|
case 'import': |
|
case 'atrule': |
|
case 'supports': |
|
this.visit(node); |
|
break; |
|
case 'comment': |
|
// only show unsuppressed comments |
|
if (!node.suppress) { |
|
this.buf += this.out(this.indent + this.visit(node) + '\n', node); |
|
} |
|
break; |
|
case 'charset': |
|
case 'literal': |
|
case 'namespace': |
|
this.buf += this.out(this.visit(node) + '\n', node); |
|
break; |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* Visit Keyframes. |
|
*/ |
|
|
|
Compiler.prototype.visitKeyframes = function(node){ |
|
if (!node.frames) return; |
|
|
|
var prefix = 'official' == node.prefix |
|
? '' |
|
: '-' + node.prefix + '-'; |
|
|
|
this.buf += this.out('@' + prefix + 'keyframes ' |
|
+ this.visit(node.val) |
|
+ (this.compress ? '{' : ' {\n'), node); |
|
|
|
this.keyframe = true; |
|
++this.indents; |
|
this.visit(node.block); |
|
--this.indents; |
|
this.keyframe = false; |
|
|
|
this.buf += this.out('}' + (this.compress ? '' : '\n')); |
|
}; |
|
|
|
/** |
|
* Visit Media. |
|
*/ |
|
|
|
Compiler.prototype.visitMedia = function(media){ |
|
var val = media.val; |
|
if (!media.hasOutput || !val.nodes.length) return; |
|
|
|
this.buf += this.out('@media ', media); |
|
this.visit(val); |
|
this.buf += this.out(this.compress ? '{' : ' {\n'); |
|
++this.indents; |
|
this.visit(media.block); |
|
--this.indents; |
|
this.buf += this.out('}' + (this.compress ? '' : '\n')); |
|
}; |
|
|
|
/** |
|
* Visit QueryList. |
|
*/ |
|
|
|
Compiler.prototype.visitQueryList = function(queries){ |
|
for (var i = 0, len = queries.nodes.length; i < len; ++i) { |
|
this.visit(queries.nodes[i]); |
|
if (len - 1 != i) this.buf += this.out(',' + (this.compress ? '' : ' ')); |
|
} |
|
}; |
|
|
|
/** |
|
* Visit Query. |
|
*/ |
|
|
|
Compiler.prototype.visitQuery = function(node){ |
|
var len = node.nodes.length; |
|
if (node.predicate) this.buf += this.out(node.predicate + ' '); |
|
if (node.type) this.buf += this.out(node.type + (len ? ' and ' : '')); |
|
for (var i = 0; i < len; ++i) { |
|
this.buf += this.out(this.visit(node.nodes[i])); |
|
if (len - 1 != i) this.buf += this.out(' and '); |
|
} |
|
}; |
|
|
|
/** |
|
* Visit Feature. |
|
*/ |
|
|
|
Compiler.prototype.visitFeature = function(node){ |
|
if (!node.expr) { |
|
return node.name; |
|
} else if (node.expr.isEmpty) { |
|
return '(' + node.name + ')'; |
|
} else { |
|
return '(' + node.name + ':' + (this.compress ? '' : ' ') + this.visit(node.expr) + ')'; |
|
} |
|
}; |
|
|
|
/** |
|
* Visit Import. |
|
*/ |
|
|
|
Compiler.prototype.visitImport = function(imported){ |
|
this.buf += this.out('@import ' + this.visit(imported.path) + ';\n', imported); |
|
}; |
|
|
|
/** |
|
* Visit Atrule. |
|
*/ |
|
|
|
Compiler.prototype.visitAtrule = function(atrule){ |
|
var newline = this.compress ? '' : '\n'; |
|
|
|
this.buf += this.out(this.indent + '@' + atrule.type, atrule); |
|
|
|
if (atrule.val) this.buf += this.out(' ' + atrule.val.trim()); |
|
|
|
if (atrule.block) { |
|
if (atrule.hasOnlyProperties) { |
|
this.visit(atrule.block); |
|
} else { |
|
this.buf += this.out(this.compress ? '{' : ' {\n'); |
|
++this.indents; |
|
this.visit(atrule.block); |
|
--this.indents; |
|
this.buf += this.out(this.indent + '}' + newline); |
|
} |
|
} else { |
|
this.buf += this.out(';' + newline); |
|
} |
|
}; |
|
|
|
/** |
|
* Visit Supports. |
|
*/ |
|
|
|
Compiler.prototype.visitSupports = function(node){ |
|
if (!node.hasOutput) return; |
|
|
|
this.buf += this.out(this.indent + '@supports ', node); |
|
this.isCondition = true; |
|
this.buf += this.out(this.visit(node.condition)); |
|
this.isCondition = false; |
|
this.buf += this.out(this.compress ? '{' : ' {\n'); |
|
++this.indents; |
|
this.visit(node.block); |
|
--this.indents; |
|
this.buf += this.out(this.indent + '}' + (this.compress ? '' : '\n')); |
|
}, |
|
|
|
/** |
|
* 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 Charset. |
|
*/ |
|
|
|
Compiler.prototype.visitCharset = function(charset){ |
|
return '@charset ' + this.visit(charset.val) + ';'; |
|
}; |
|
|
|
/** |
|
* Visit Namespace. |
|
*/ |
|
|
|
Compiler.prototype.visitNamespace = function(namespace){ |
|
return '@namespace ' |
|
+ (namespace.prefix ? this.visit(namespace.prefix) + ' ' : '') |
|
+ this.visit(namespace.val) + ';'; |
|
}; |
|
|
|
/** |
|
* Visit Literal. |
|
*/ |
|
|
|
Compiler.prototype.visitLiteral = function(lit){ |
|
var val = lit.val; |
|
if (lit.css) 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) { |
|
// Always return '0' unless the unit is a percentage or time |
|
if ('%' != type && 's' != type && 'ms' != type && 0 == n) return '0'; |
|
// Omit leading '0' on floats |
|
if (float && n < 1 && n > -1) { |
|
return n.toString().replace('0.', '.') + type; |
|
} |
|
} |
|
|
|
return (float ? parseFloat(n.toFixed(15)) : n).toString() + type; |
|
}; |
|
|
|
/** |
|
* Visit Group. |
|
*/ |
|
|
|
Compiler.prototype.visitGroup = function(group){ |
|
var stack = this.keyframe ? [] : this.stack |
|
, comma = this.compress ? ',' : ',\n'; |
|
|
|
stack.push(group.nodes); |
|
|
|
// selectors |
|
if (group.block.hasProperties) { |
|
var selectors = utils.compileSelectors.call(this, stack) |
|
, len = selectors.length; |
|
|
|
if (len) { |
|
if (this.keyframe) comma = this.compress ? ',' : ', '; |
|
|
|
for (var i = 0; i < len; ++i) { |
|
var selector = selectors[i] |
|
, last = (i == len - 1); |
|
|
|
// keyframe blocks (10%, 20% { ... }) |
|
if (this.keyframe) selector = i ? selector.trim() : selector; |
|
|
|
this.buf += this.out(selector + (last ? '' : comma), group.nodes[i]); |
|
} |
|
} 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; |
|
|
|
var space = self.isURL || (self.isCondition |
|
&& (')' == nodes[i + 1] || '(' == node)) |
|
? '' : ' '; |
|
|
|
buf.push(expr.isList |
|
? (self.compress ? ',' : ', ') |
|
: space); |
|
}); |
|
|
|
return buf.join(''); |
|
}; |
|
|
|
/** |
|
* Visit Arguments. |
|
*/ |
|
|
|
Compiler.prototype.visitArguments = Compiler.prototype.visitExpression; |
|
|
|
/** |
|
* Visit Property. |
|
*/ |
|
|
|
Compiler.prototype.visitProperty = function(prop){ |
|
var val = this.visit(prop.expr).trim() |
|
, name = (prop.name || prop.segments.join('')) |
|
, arr = []; |
|
arr.push( |
|
this.out(this.indent), |
|
this.out(name + (this.compress ? ':' : ': '), prop), |
|
this.out(val, prop.expr), |
|
this.out(this.compress ? (this.last ? '' : ';') : ';') |
|
); |
|
return arr.join(''); |
|
}; |
|
|
|
/** |
|
* Debug info. |
|
*/ |
|
|
|
Compiler.prototype.debugInfo = function(node){ |
|
|
|
var path = node.filename == 'stdin' ? 'stdin' : fs.realpathSync(node.filename) |
|
, line = (node.nodes && node.nodes.length ? node.nodes[0].lineno : node.lineno) || 1; |
|
|
|
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, function(m) { |
|
return '\\' + (m === '\\' ? '\/' : m) |
|
}); |
|
line = '\\00003' + line; |
|
this.buf += '\n@media -stylus-debug-info' |
|
+ '{filename{font-family:' + path |
|
+ '}line{font-family:' + line + '}}\n'; |
|
} |
|
}
|
|
|