(function (tree) { tree.Ruleset = function (selectors, rules, strictImports) { this.selectors = selectors; this.rules = rules; this._lookups = {}; this.strictImports = strictImports; }; tree.Ruleset.prototype = { type: "Ruleset", accept: function (visitor) { if (this.paths) { visitor.visitArray(this.paths, true); } else if (this.selectors) { this.selectors = visitor.visitArray(this.selectors); } if (this.rules && this.rules.length) { this.rules = visitor.visitArray(this.rules); } }, eval: function (env) { var thisSelectors = this.selectors, selectors, selCnt, i, defaultFunc = tree.defaultFunc; if (thisSelectors && (selCnt = thisSelectors.length)) { selectors = []; defaultFunc.error({ type: "Syntax", message: "it is currently only allowed in parametric mixin guards," }); for (i = 0; i < selCnt; i++) { selectors.push(thisSelectors[i].eval(env)); } defaultFunc.reset(); } var rules = this.rules ? this.rules.slice(0) : null, ruleset = new(tree.Ruleset)(selectors, rules, this.strictImports), rule, subRule; ruleset.originalRuleset = this; ruleset.root = this.root; ruleset.firstRoot = this.firstRoot; ruleset.allowImports = this.allowImports; if(this.debugInfo) { ruleset.debugInfo = this.debugInfo; } // push the current ruleset to the frames stack var envFrames = env.frames; envFrames.unshift(ruleset); // currrent selectors var envSelectors = env.selectors; if (!envSelectors) { env.selectors = envSelectors = []; } envSelectors.unshift(this.selectors); // Evaluate imports if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) { ruleset.evalImports(env); } // Store the frames around mixin definitions, // so they can be evaluated like closures when the time comes. var rsRules = ruleset.rules, rsRuleCnt = rsRules ? rsRules.length : 0; for (i = 0; i < rsRuleCnt; i++) { if (rsRules[i] instanceof tree.mixin.Definition) { rsRules[i].frames = envFrames.slice(0); } } var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0; // Evaluate mixin calls. for (i = 0; i < rsRuleCnt; i++) { if (rsRules[i] instanceof tree.mixin.Call) { /*jshint loopfunc:true */ rules = rsRules[i].eval(env).filter(function(r) { if ((r instanceof tree.Rule) && r.variable) { // do not pollute the scope if the variable is // already there. consider returning false here // but we need a way to "return" variable from mixins return !(ruleset.variable(r.name)); } return true; }); rsRules.splice.apply(rsRules, [i, 1].concat(rules)); rsRuleCnt += rules.length - 1; i += rules.length-1; ruleset.resetCache(); } } // Evaluate everything else for (i = 0; i < rsRules.length; i++) { rule = rsRules[i]; if (! (rule instanceof tree.mixin.Definition)) { rsRules[i] = rule = rule.eval ? rule.eval(env) : rule; // for rulesets, check if it is a css guard and can be removed if (rule instanceof tree.Ruleset && rule.selectors && rule.selectors.length === 1) { // check if it can be folded in (e.g. & where) if (rule.selectors[0].isJustParentSelector()) { rsRules.splice(i--, 1); // cannot call if there is no selector, so we can just continue if (!rule.selectors[0].evaldCondition) { continue; } for(var j = 0; j < rule.rules.length; j++) { subRule = rule.rules[j]; if (!(subRule instanceof tree.Rule) || !subRule.variable) { rsRules.splice(++i, 0, subRule); } } } } } } // Pop the stack envFrames.shift(); envSelectors.shift(); if (env.mediaBlocks) { for (i = mediaBlockCount; i < env.mediaBlocks.length; i++) { env.mediaBlocks[i].bubbleSelectors(selectors); } } return ruleset; }, evalImports: function(env) { var rules = this.rules, i, importRules; if (!rules) { return; } for (i = 0; i < rules.length; i++) { if (rules[i] instanceof tree.Import) { importRules = rules[i].eval(env); if (importRules && importRules.length) { rules.splice.apply(rules, [i, 1].concat(importRules)); i+= importRules.length-1; } else { rules.splice(i, 1, importRules); } this.resetCache(); } } }, makeImportant: function() { return new tree.Ruleset(this.selectors, this.rules.map(function (r) { if (r.makeImportant) { return r.makeImportant(); } else { return r; } }), this.strictImports); }, matchArgs: function (args) { return !args || args.length === 0; }, // lets you call a css selector with a guard matchCondition: function (args, env) { var lastSelector = this.selectors[this.selectors.length-1]; if (!lastSelector.evaldCondition) { return false; } if (lastSelector.condition && !lastSelector.condition.eval( new(tree.evalEnv)(env, env.frames))) { return false; } return true; }, resetCache: function () { this._rulesets = null; this._variables = null; this._lookups = {}; }, variables: function () { if (!this._variables) { this._variables = !this.rules ? {} : this.rules.reduce(function (hash, r) { if (r instanceof tree.Rule && r.variable === true) { hash[r.name] = r; } return hash; }, {}); } return this._variables; }, variable: function (name) { return this.variables()[name]; }, rulesets: function () { if (!this.rules) { return null; } var _Ruleset = tree.Ruleset, _MixinDefinition = tree.mixin.Definition, filtRules = [], rules = this.rules, cnt = rules.length, i, rule; for (i = 0; i < cnt; i++) { rule = rules[i]; if ((rule instanceof _Ruleset) || (rule instanceof _MixinDefinition)) { filtRules.push(rule); } } return filtRules; }, prependRule: function (rule) { var rules = this.rules; if (rules) { rules.unshift(rule); } else { this.rules = [ rule ]; } }, find: function (selector, self) { self = self || this; var rules = [], match, key = selector.toCSS(); if (key in this._lookups) { return this._lookups[key]; } this.rulesets().forEach(function (rule) { if (rule !== self) { for (var j = 0; j < rule.selectors.length; j++) { match = selector.match(rule.selectors[j]); if (match) { if (selector.elements.length > match) { Array.prototype.push.apply(rules, rule.find( new(tree.Selector)(selector.elements.slice(match)), self)); } else { rules.push(rule); } break; } } } }); this._lookups[key] = rules; return rules; }, genCSS: function (env, output) { var i, j, ruleNodes = [], rulesetNodes = [], rulesetNodeCnt, debugInfo, // Line number debugging rule, path; env.tabLevel = (env.tabLevel || 0); if (!this.root) { env.tabLevel++; } var tabRuleStr = env.compress ? '' : Array(env.tabLevel + 1).join(" "), tabSetStr = env.compress ? '' : Array(env.tabLevel).join(" "), sep; for (i = 0; i < this.rules.length; i++) { rule = this.rules[i]; if (rule.rules || (rule instanceof tree.Media) || rule instanceof tree.Directive || (this.root && rule instanceof tree.Comment)) { rulesetNodes.push(rule); } else { ruleNodes.push(rule); } } // If this is the root node, we don't render // a selector, or {}. if (!this.root) { debugInfo = tree.debugInfo(env, this, tabSetStr); if (debugInfo) { output.add(debugInfo); output.add(tabSetStr); } var paths = this.paths, pathCnt = paths.length, pathSubCnt; sep = env.compress ? ',' : (',\n' + tabSetStr); for (i = 0; i < pathCnt; i++) { path = paths[i]; if (!(pathSubCnt = path.length)) { continue; } if (i > 0) { output.add(sep); } env.firstSelector = true; path[0].genCSS(env, output); env.firstSelector = false; for (j = 1; j < pathSubCnt; j++) { path[j].genCSS(env, output); } } output.add((env.compress ? '{' : ' {\n') + tabRuleStr); } // Compile rules and rulesets for (i = 0; i < ruleNodes.length; i++) { rule = ruleNodes[i]; // @page{ directive ends up with root elements inside it, a mix of rules and rulesets // In this instance we do not know whether it is the last property if (i + 1 === ruleNodes.length && (!this.root || rulesetNodes.length === 0 || this.firstRoot)) { env.lastRule = true; } if (rule.genCSS) { rule.genCSS(env, output); } else if (rule.value) { output.add(rule.value.toString()); } if (!env.lastRule) { output.add(env.compress ? '' : ('\n' + tabRuleStr)); } else { env.lastRule = false; } } if (!this.root) { output.add((env.compress ? '}' : '\n' + tabSetStr + '}')); env.tabLevel--; } sep = (env.compress ? "" : "\n") + (this.root ? tabRuleStr : tabSetStr); rulesetNodeCnt = rulesetNodes.length; if (rulesetNodeCnt) { if (ruleNodes.length && sep) { output.add(sep); } rulesetNodes[0].genCSS(env, output); for (i = 1; i < rulesetNodeCnt; i++) { if (sep) { output.add(sep); } rulesetNodes[i].genCSS(env, output); } } if (!output.isEmpty() && !env.compress && this.firstRoot) { output.add('\n'); } }, toCSS: tree.toCSS, markReferenced: function () { for (var s = 0; s < this.selectors.length; s++) { this.selectors[s].markReferenced(); } }, joinSelectors: function (paths, context, selectors) { for (var s = 0; s < selectors.length; s++) { this.joinSelector(paths, context, selectors[s]); } }, joinSelector: function (paths, context, selector) { var i, j, k, hasParentSelector, newSelectors, el, sel, parentSel, newSelectorPath, afterParentJoin, newJoinedSelector, newJoinedSelectorEmpty, lastSelector, currentElements, selectorsMultiplied; for (i = 0; i < selector.elements.length; i++) { el = selector.elements[i]; if (el.value === '&') { hasParentSelector = true; } } if (!hasParentSelector) { if (context.length > 0) { for (i = 0; i < context.length; i++) { paths.push(context[i].concat(selector)); } } else { paths.push([selector]); } return; } // The paths are [[Selector]] // The first list is a list of comma seperated selectors // The inner list is a list of inheritance seperated selectors // e.g. // .a, .b { // .c { // } // } // == [[.a] [.c]] [[.b] [.c]] // // the elements from the current selector so far currentElements = []; // the current list of new selectors to add to the path. // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors // by the parents newSelectors = [[]]; for (i = 0; i < selector.elements.length; i++) { el = selector.elements[i]; // non parent reference elements just get added if (el.value !== "&") { currentElements.push(el); } else { // the new list of selectors to add selectorsMultiplied = []; // merge the current list of non parent selector elements // on to the current list of selectors to add if (currentElements.length > 0) { this.mergeElementsOnToSelectors(currentElements, newSelectors); } // loop through our current selectors for (j = 0; j < newSelectors.length; j++) { sel = newSelectors[j]; // if we don't have any parent paths, the & might be in a mixin so that it can be used // whether there are parents or not if (context.length === 0) { // the combinator used on el should now be applied to the next element instead so that // it is not lost if (sel.length > 0) { sel[0].elements = sel[0].elements.slice(0); sel[0].elements.push(new(tree.Element)(el.combinator, '', el.index, el.currentFileInfo)); } selectorsMultiplied.push(sel); } else { // and the parent selectors for (k = 0; k < context.length; k++) { parentSel = context[k]; // We need to put the current selectors // then join the last selector's elements on to the parents selectors // our new selector path newSelectorPath = []; // selectors from the parent after the join afterParentJoin = []; newJoinedSelectorEmpty = true; //construct the joined selector - if & is the first thing this will be empty, // if not newJoinedSelector will be the last set of elements in the selector if (sel.length > 0) { newSelectorPath = sel.slice(0); lastSelector = newSelectorPath.pop(); newJoinedSelector = selector.createDerived(lastSelector.elements.slice(0)); newJoinedSelectorEmpty = false; } else { newJoinedSelector = selector.createDerived([]); } //put together the parent selectors after the join if (parentSel.length > 1) { afterParentJoin = afterParentJoin.concat(parentSel.slice(1)); } if (parentSel.length > 0) { newJoinedSelectorEmpty = false; // join the elements so far with the first part of the parent newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, el.index, el.currentFileInfo)); newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1)); } if (!newJoinedSelectorEmpty) { // now add the joined selector newSelectorPath.push(newJoinedSelector); } // and the rest of the parent newSelectorPath = newSelectorPath.concat(afterParentJoin); // add that to our new set of selectors selectorsMultiplied.push(newSelectorPath); } } } // our new selectors has been multiplied, so reset the state newSelectors = selectorsMultiplied; currentElements = []; } } // if we have any elements left over (e.g. .a& .b == .b) // add them on to all the current selectors if (currentElements.length > 0) { this.mergeElementsOnToSelectors(currentElements, newSelectors); } for (i = 0; i < newSelectors.length; i++) { if (newSelectors[i].length > 0) { paths.push(newSelectors[i]); } } }, mergeElementsOnToSelectors: function(elements, selectors) { var i, sel; if (selectors.length === 0) { selectors.push([ new(tree.Selector)(elements) ]); return; } for (i = 0; i < selectors.length; i++) { sel = selectors[i]; // if the previous thing in sel is a parent this needs to join on to it if (sel.length > 0) { sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements)); } else { sel.push(new(tree.Selector)(elements)); } } } }; })(require('../tree'));