/*! * Stylus - Normalizer * Copyright(c) 2010 LearnBoost * 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(){ // normalize cached imports if ('block' == this.root.nodeName) this.root.constructor = nodes.Root; 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 'unit': case 'atblock': 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 'group': case 'unit': case 'atblock': continue; default: ret.push(this.visit(node)); } } } // nesting for (var i = 0, len = block.nodes.length; i < len; ++i) { node = block.nodes[i]; // normalize cached imports if ('root' == node.nodeName) node.constructor = nodes.Block; ret.push(this.visit(node)); } return block; }; /** * Visit Group. */ Normalizer.prototype.visitGroup = function(group){ var stack = this.stack , map = this.map , parts; // normalize interpolated selectors with comma group.nodes.forEach(function(selector, i){ if (!~selector.val.indexOf(',')) return; if (~selector.val.indexOf('\\,')) { selector.val = selector.val.replace(/\\,/g, ','); return; } parts = selector.val.split(','); for (var k = 0, len = parts.length; k < len; ++k){ var part = parts[k].trim(); // Treat parts without `&` as interpolated from the root if (!~part.indexOf('&') && part.charAt(0) != '/') { part = '/' + part; } var s = new nodes.Selector([part]); s.val = part; s.block = group.block; group.nodes[i++] = s; } }); stack.push(group.nodes); var selectors = utils.compileSelectors(stack, true); // 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 = [] , self = this; function filterProps(block) { block.nodes.forEach(function(node) { node = self.visit(node); switch (node.nodeName) { case 'property': props.push(node); break; case 'block': filterProps(node); break; default: other.push(node); } }); } filterProps(media.block); // 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; } /** * Visit Keyframes. */ Normalizer.prototype.visitKeyframes = function(node){ var frames = node.block.nodes.filter(function(frame){ return frame.block && frame.block.hasProperties; }); node.frames = frames.length; return node; }; /** * Apply `group` extensions. * * @param {Group} group * @param {Array} selectors * @api private */ Normalizer.prototype.extend = function(group, selectors){ var map = this.map , self = this; group.block.node.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){ if (!group.nodes.some(function(n){ return n.val == selector })) { self.extend(group, selectors); group.push(node); } }); }); }); };