less
Version:
Leaner CSS
444 lines • 23.5 kB
JavaScript
"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