/*!
* Stylus - Normalizer
* Copyright(c) 2010 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Visitor = require('./')
, nodes = require('../nodes')
, utils = require('../utils')
, fs = require('fs');
/**
* Initialize a new `Normalizer` with the given `root` Node.
*
* This visitor implements the first stage of the duel-stage
* compiler, tasked with stripping the "garbage" from
* the evaluated nodes, ditching null rules, resolving
* ruleset selectors etc. This step performs the logic
* necessary to facilitate the "@extend" functionality,
* as these must be resolved _before_ buffering output.
*
* @param {Node} root
* @api public
*/
var Normalizer = module.exports = function Normalizer(root, options) {
options = options || {};
Visitor.call(this, root);
this.stack = [];
this.extends = {};
this.map = {};
};
/**
* Inherit from `Visitor.prototype`.
*/
Normalizer.prototype.__proto__ = Visitor.prototype;
/**
* Normalize the node tree.
*
* @return {Node}
* @api private
*/
Normalizer.prototype.normalize = function(){
return this.visit(this.root);
};
/**
* Visit Root.
*/
Normalizer.prototype.visitRoot = function(block){
var ret = new nodes.Root
, node;
for (var i = 0, len = block.nodes.length; i < len; ++i) {
node = block.nodes[i];
switch (node.nodeName) {
case 'null':
case 'expression':
case 'function':
case 'jsliteral':
case 'unit':
continue;
default:
ret.push(this.visit(node));
}
}
return ret;
};
/**
* Visit Block.
*/
Normalizer.prototype.visitBlock = function(block){
var ret = new nodes.Block
, node;
if (block.hasProperties) {
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;
default:
ret.push(this.visit(node));
}
}
}
// nesting
for (var i = 0, len = block.nodes.length; i < len; ++i) {
node = block.nodes[i];
ret.push(this.visit(node));
}
return block;
};
/**
* Visit Group.
*/
Normalizer.prototype.visitGroup = function(group){
// TODO: clean this mess up
var stack = this.stack
, map = this.map
, self = this;
stack.push(group.nodes);
var selectors = this.compileSelectors(stack);
// map for extension lookup
selectors.forEach(function(selector){
map[selector] = map[selector] || [];
map[selector].push(group);
});
// extensions
this.extend(group, selectors);
group.block = this.visit(group.block);
stack.pop();
return group;
};
/**
* Visit Media.
*/
Normalizer.prototype.visitMedia = function(media){
var props = []
, other = [];
media.block.nodes.forEach(function(node, i) {
if ('property' == node.nodeName) {
props.push(node);
} else {
other.push(node);
}
});
// Fake self-referencing group to contain
// any props that are floating
// directly on the @media declaration
if (props.length) {
var selfLiteral = new nodes.Literal('&');
selfLiteral.lineno = media.lineno;
selfLiteral.filename = media.filename;
var selfSelector = new nodes.Selector(selfLiteral);
selfSelector.lineno = media.lineno;
selfSelector.filename = media.filename;
selfSelector.val = selfLiteral.val;
var propertyGroup = new nodes.Group;
propertyGroup.lineno = media.lineno;
propertyGroup.filename = media.filename;
var propertyBlock = new nodes.Block(media.block, propertyGroup);
propertyBlock.lineno = media.lineno;
propertyBlock.filename = media.filename;
props.forEach(function(prop){
propertyBlock.push(prop);
});
propertyGroup.push(selfSelector);
propertyGroup.block = propertyBlock;
media.block.nodes = [];
media.block.push(propertyGroup);
other.forEach(function(node){
media.block.push(node);
});
}
return media;
}
/**
* Apply `group` extensions.
*
* @param {Group} group
* @param {Array} selectors
* @api private
*/
Normalizer.prototype.extend = function(group, selectors){
var map = this.map
, self = this;
group.extends.forEach(function(extend){
var groups = map[extend];
if (!groups) throw new Error('Failed to @extend "' + extend + '"');
selectors.forEach(function(selector){
var node = new nodes.Selector;
node.val = selector;
node.inherits = false;
groups.forEach(function(group){
self.extend(group, selectors);
group.push(node);
});
});
});
};
/**
* 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
*/
Normalizer.prototype.compileSelectors = function(arr){
// TODO: remove this duplication
var stack = this.stack
, self = this
, selectors = []
, buf = [];
function compile(arr, i) {
if (i) {
arr[i].forEach(function(selector){
buf.unshift(selector.val);
compile(arr, i - 1);
buf.shift();
});
} else {
arr[0].forEach(function(selector){
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();
}
}
}
selectors.push(str.trimRight());
});
}
}
compile(arr, arr.length - 1);
return selectors;
};