UNPKG

less

Version:
444 lines 23.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); /* eslint-disable no-unused-vars */ /** * @todo - Remove unused when JSDoc types are added for visitor methods */ var tree_1 = tslib_1.__importDefault(require("../tree")); var visitor_1 = tslib_1.__importDefault(require("./visitor")); var logger_1 = tslib_1.__importDefault(require("../logger")); var utils = tslib_1.__importStar(require("../utils")); /* jshint loopfunc:true */ var ExtendFinderVisitor = /** @class */ (function () { function ExtendFinderVisitor() { this._visitor = new visitor_1.default(this); this.contexts = []; this.allExtendsStack = [[]]; } ExtendFinderVisitor.prototype.run = function (root) { root = this._visitor.visit(root); root.allExtends = this.allExtendsStack[0]; return root; }; ExtendFinderVisitor.prototype.visitDeclaration = function (declNode, visitArgs) { visitArgs.visitDeeper = false; }; ExtendFinderVisitor.prototype.visitMixinDefinition = function (mixinDefinitionNode, visitArgs) { visitArgs.visitDeeper = false; }; ExtendFinderVisitor.prototype.visitRuleset = function (rulesetNode, visitArgs) { if (rulesetNode.root) { return; } var i; var j; var extend; var allSelectorsExtendList = []; var extendList; // get &:extend(.a); rules which apply to all selectors in this ruleset var rules = rulesetNode.rules, ruleCnt = rules ? rules.length : 0; for (i = 0; i < ruleCnt; i++) { if (rulesetNode.rules[i] instanceof tree_1.default.Extend) { allSelectorsExtendList.push(rules[i]); rulesetNode.extendOnEveryPath = true; } } // now find every selector and apply the extends that apply to all extends // and the ones which apply to an individual extend var paths = rulesetNode.paths; for (i = 0; i < paths.length; i++) { var selectorPath = paths[i], selector = selectorPath[selectorPath.length - 1], selExtendList = selector.extendList; extendList = selExtendList ? utils.copyArray(selExtendList).concat(allSelectorsExtendList) : allSelectorsExtendList; if (extendList) { extendList = extendList.map(function (allSelectorsExtend) { return allSelectorsExtend.clone(); }); } for (j = 0; j < extendList.length; j++) { this.foundExtends = true; extend = extendList[j]; extend.findSelfSelectors(selectorPath); extend.ruleset = rulesetNode; if (j === 0) { extend.firstExtendOnThisSelectorPath = true; } this.allExtendsStack[this.allExtendsStack.length - 1].push(extend); } } this.contexts.push(rulesetNode.selectors); }; ExtendFinderVisitor.prototype.visitRulesetOut = function (rulesetNode) { if (!rulesetNode.root) { this.contexts.length = this.contexts.length - 1; } }; ExtendFinderVisitor.prototype.visitMedia = function (mediaNode, visitArgs) { mediaNode.allExtends = []; this.allExtendsStack.push(mediaNode.allExtends); }; ExtendFinderVisitor.prototype.visitMediaOut = function (mediaNode) { this.allExtendsStack.length = this.allExtendsStack.length - 1; }; ExtendFinderVisitor.prototype.visitAtRule = function (atRuleNode, visitArgs) { atRuleNode.allExtends = []; this.allExtendsStack.push(atRuleNode.allExtends); }; ExtendFinderVisitor.prototype.visitAtRuleOut = function (atRuleNode) { this.allExtendsStack.length = this.allExtendsStack.length - 1; }; return ExtendFinderVisitor; }()); var ProcessExtendsVisitor = /** @class */ (function () { function ProcessExtendsVisitor() { this._visitor = new visitor_1.default(this); } ProcessExtendsVisitor.prototype.run = function (root) { var extendFinder = new ExtendFinderVisitor(); this.extendIndices = {}; extendFinder.run(root); if (!extendFinder.foundExtends) { return root; } root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends)); this.allExtendsStack = [root.allExtends]; var newRoot = this._visitor.visit(root); this.checkExtendsForNonMatched(root.allExtends); return newRoot; }; ProcessExtendsVisitor.prototype.checkExtendsForNonMatched = function (extendList) { var indices = this.extendIndices; extendList.filter(function (extend) { return !extend.hasFoundMatches && extend.parent_ids.length == 1; }).forEach(function (extend) { var selector = '_unknown_'; try { selector = extend.selector.toCSS({}); } catch (_) { } if (!indices["".concat(extend.index, " ").concat(selector)]) { indices["".concat(extend.index, " ").concat(selector)] = true; /** * @todo Shouldn't this be an error? To alert the developer * that they may have made an error in the selector they are * targeting? */ logger_1.default.warn("WARNING: extend '".concat(selector, "' has no matches")); } }); }; ProcessExtendsVisitor.prototype.doExtendChaining = function (extendsList, extendsListTarget, iterationCount) { // // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering // and pasting the selector we would do normally, but we are also adding an extend with the same target selector // this means this new extend can then go and alter other extends // // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already // processed if we look at each selector at a time, as is done in visitRuleset var extendIndex; var targetExtendIndex; var matches; var extendsToAdd = []; var newSelector; var extendVisitor = this; var selectorPath; var extend; var targetExtend; var newExtend; iterationCount = iterationCount || 0; // loop through comparing every extend with every target extend. // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one // and the second is the target. // the separation into two lists allows us to process a subset of chains with a bigger set, as is the // case when processing media queries for (extendIndex = 0; extendIndex < extendsList.length; extendIndex++) { for (targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++) { extend = extendsList[extendIndex]; targetExtend = extendsListTarget[targetExtendIndex]; // look for circular references if (extend.parent_ids.indexOf(targetExtend.object_id) >= 0) { continue; } // find a match in the target extends self selector (the bit before :extend) selectorPath = [targetExtend.selfSelectors[0]]; matches = extendVisitor.findMatch(extend, selectorPath); if (matches.length) { extend.hasFoundMatches = true; // we found a match, so for each self selector.. extend.selfSelectors.forEach(function (selfSelector) { var info = targetExtend.visibilityInfo(); // process the extend as usual newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector, extend.isVisible()); // but now we create a new extend from it newExtend = new (tree_1.default.Extend)(targetExtend.selector, targetExtend.option, 0, targetExtend.fileInfo(), info); newExtend.selfSelectors = newSelector; // add the extend onto the list of extends for that selector newSelector[newSelector.length - 1].extendList = [newExtend]; // record that we need to add it. extendsToAdd.push(newExtend); newExtend.ruleset = targetExtend.ruleset; // remember its parents for circular references newExtend.parent_ids = newExtend.parent_ids.concat(targetExtend.parent_ids, extend.parent_ids); // only process the selector once.. if we have :extend(.a,.b) then multiple // extends will look at the same selector path, so when extending // we know that any others will be duplicates in terms of what is added to the css if (targetExtend.firstExtendOnThisSelectorPath) { newExtend.firstExtendOnThisSelectorPath = true; targetExtend.ruleset.paths.push(newSelector); } }); } } } if (extendsToAdd.length) { // try to detect circular references to stop a stack overflow. // may no longer be needed. this.extendChainCount++; if (iterationCount > 100) { var selectorOne = '{unable to calculate}'; var selectorTwo = '{unable to calculate}'; try { selectorOne = extendsToAdd[0].selfSelectors[0].toCSS(); selectorTwo = extendsToAdd[0].selector.toCSS(); } catch (e) { } throw { message: "extend circular reference detected. One of the circular extends is currently:".concat(selectorOne, ":extend(").concat(selectorTwo, ")") }; } // now process the new extends on the existing rules so that we can handle a extending b extending c extending // d extending e... return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount + 1)); } else { return extendsToAdd; } }; ProcessExtendsVisitor.prototype.visitDeclaration = function (ruleNode, visitArgs) { visitArgs.visitDeeper = false; }; ProcessExtendsVisitor.prototype.visitMixinDefinition = function (mixinDefinitionNode, visitArgs) { visitArgs.visitDeeper = false; }; ProcessExtendsVisitor.prototype.visitSelector = function (selectorNode, visitArgs) { visitArgs.visitDeeper = false; }; ProcessExtendsVisitor.prototype.visitRuleset = function (rulesetNode, visitArgs) { if (rulesetNode.root) { return; } var matches; var pathIndex; var extendIndex; var allExtends = this.allExtendsStack[this.allExtendsStack.length - 1]; var selectorsToAdd = []; var extendVisitor = this; var selectorPath; // look at each selector path in the ruleset, find any extend matches and then copy, find and replace for (extendIndex = 0; extendIndex < allExtends.length; extendIndex++) { for (pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) { selectorPath = rulesetNode.paths[pathIndex]; // extending extends happens initially, before the main pass if (rulesetNode.extendOnEveryPath) { continue; } var extendList = selectorPath[selectorPath.length - 1].extendList; if (extendList && extendList.length) { continue; } matches = this.findMatch(allExtends[extendIndex], selectorPath); if (matches.length) { allExtends[extendIndex].hasFoundMatches = true; allExtends[extendIndex].selfSelectors.forEach(function (selfSelector) { var extendedSelectors; extendedSelectors = extendVisitor.extendSelector(matches, selectorPath, selfSelector, allExtends[extendIndex].isVisible()); selectorsToAdd.push(extendedSelectors); }); } } } rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd); }; ProcessExtendsVisitor.prototype.findMatch = function (extend, haystackSelectorPath) { // // look through the haystack selector path to try and find the needle - extend.selector // returns an array of selector matches that can then be replaced // var haystackSelectorIndex; var hackstackSelector; var hackstackElementIndex; var haystackElement; var targetCombinator; var i; var extendVisitor = this; var needleElements = extend.selector.elements; var potentialMatches = []; var potentialMatch; var matches = []; // loop through the haystack elements for (haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) { hackstackSelector = haystackSelectorPath[haystackSelectorIndex]; for (hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) { haystackElement = hackstackSelector.elements[hackstackElementIndex]; // if we allow elements before our match we can add a potential match every time. otherwise only at the first element. if (extend.allowBefore || (haystackSelectorIndex === 0 && hackstackElementIndex === 0)) { potentialMatches.push({ pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0, initialCombinator: haystackElement.combinator }); } for (i = 0; i < potentialMatches.length; i++) { potentialMatch = potentialMatches[i]; // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to // work out what the resulting combinator will be targetCombinator = haystackElement.combinator.value; if (targetCombinator === '' && hackstackElementIndex === 0) { targetCombinator = ' '; } // if we don't match, null our match to indicate failure if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) || (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) { potentialMatch = null; } else { potentialMatch.matched++; } // if we are still valid and have finished, test whether we have elements after and whether these are allowed if (potentialMatch) { potentialMatch.finished = potentialMatch.matched === needleElements.length; if (potentialMatch.finished && (!extend.allowAfter && (hackstackElementIndex + 1 < hackstackSelector.elements.length || haystackSelectorIndex + 1 < haystackSelectorPath.length))) { potentialMatch = null; } } // if null we remove, if not, we are still valid, so either push as a valid match or continue if (potentialMatch) { if (potentialMatch.finished) { potentialMatch.length = needleElements.length; potentialMatch.endPathIndex = haystackSelectorIndex; potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again matches.push(potentialMatch); } } else { potentialMatches.splice(i, 1); i--; } } } } return matches; }; ProcessExtendsVisitor.prototype.isElementValuesEqual = function (elementValue1, elementValue2) { if (typeof elementValue1 === 'string' || typeof elementValue2 === 'string') { return elementValue1 === elementValue2; } if (elementValue1 instanceof tree_1.default.Attribute) { if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) { return false; } if (!elementValue1.value || !elementValue2.value) { if (elementValue1.value || elementValue2.value) { return false; } return true; } elementValue1 = elementValue1.value.value || elementValue1.value; elementValue2 = elementValue2.value.value || elementValue2.value; return elementValue1 === elementValue2; } elementValue1 = elementValue1.value; elementValue2 = elementValue2.value; if (elementValue1 instanceof tree_1.default.Selector) { if (!(elementValue2 instanceof tree_1.default.Selector) || elementValue1.elements.length !== elementValue2.elements.length) { return false; } for (var i = 0; i < elementValue1.elements.length; i++) { if (elementValue1.elements[i].combinator.value !== elementValue2.elements[i].combinator.value) { if (i !== 0 || (elementValue1.elements[i].combinator.value || ' ') !== (elementValue2.elements[i].combinator.value || ' ')) { return false; } } if (!this.isElementValuesEqual(elementValue1.elements[i].value, elementValue2.elements[i].value)) { return false; } } return true; } return false; }; ProcessExtendsVisitor.prototype.extendSelector = function (matches, selectorPath, replacementSelector, isVisible) { // for a set of matches, replace each match with the replacement selector var currentSelectorPathIndex = 0, currentSelectorPathElementIndex = 0, path = [], matchIndex, selector, firstElement, match, newElements; for (matchIndex = 0; matchIndex < matches.length; matchIndex++) { match = matches[matchIndex]; selector = selectorPath[match.pathIndex]; firstElement = new tree_1.default.Element(match.initialCombinator, replacementSelector.elements[0].value, replacementSelector.elements[0].isVariable, replacementSelector.elements[0].getIndex(), replacementSelector.elements[0].fileInfo()); if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) { path[path.length - 1].elements = path[path.length - 1] .elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex)); currentSelectorPathElementIndex = 0; currentSelectorPathIndex++; } newElements = selector.elements .slice(currentSelectorPathElementIndex, match.index) .concat([firstElement]) .concat(replacementSelector.elements.slice(1)); if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) { path[path.length - 1].elements = path[path.length - 1].elements.concat(newElements); } else { path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex)); path.push(new tree_1.default.Selector(newElements)); } currentSelectorPathIndex = match.endPathIndex; currentSelectorPathElementIndex = match.endPathElementIndex; if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) { currentSelectorPathElementIndex = 0; currentSelectorPathIndex++; } } if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) { path[path.length - 1].elements = path[path.length - 1] .elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex)); currentSelectorPathIndex++; } path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length)); path = path.map(function (currentValue) { // we can re-use elements here, because the visibility property matters only for selectors var derived = currentValue.createDerived(currentValue.elements); if (isVisible) { derived.ensureVisibility(); } else { derived.ensureInvisibility(); } return derived; }); return path; }; ProcessExtendsVisitor.prototype.visitMedia = function (mediaNode, visitArgs) { var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length - 1]); newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends)); this.allExtendsStack.push(newAllExtends); }; ProcessExtendsVisitor.prototype.visitMediaOut = function (mediaNode) { var lastIndex = this.allExtendsStack.length - 1; this.allExtendsStack.length = lastIndex; }; ProcessExtendsVisitor.prototype.visitAtRule = function (atRuleNode, visitArgs) { var newAllExtends = atRuleNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length - 1]); newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, atRuleNode.allExtends)); this.allExtendsStack.push(newAllExtends); }; ProcessExtendsVisitor.prototype.visitAtRuleOut = function (atRuleNode) { var lastIndex = this.allExtendsStack.length - 1; this.allExtendsStack.length = lastIndex; }; return ProcessExtendsVisitor; }()); exports.default = ProcessExtendsVisitor; //# sourceMappingURL=extend-visitor.js.map