UNPKG

23.1 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3var tslib_1 = require("tslib");
4var tree_1 = tslib_1.__importDefault(require("../tree"));
5var visitor_1 = tslib_1.__importDefault(require("./visitor"));
6var logger_1 = tslib_1.__importDefault(require("../logger"));
7var utils = tslib_1.__importStar(require("../utils"));
8/* jshint loopfunc:true */
9var ExtendFinderVisitor = /** @class */ (function () {
10 function ExtendFinderVisitor() {
11 this._visitor = new visitor_1.default(this);
12 this.contexts = [];
13 this.allExtendsStack = [[]];
14 }
15 ExtendFinderVisitor.prototype.run = function (root) {
16 root = this._visitor.visit(root);
17 root.allExtends = this.allExtendsStack[0];
18 return root;
19 };
20 ExtendFinderVisitor.prototype.visitDeclaration = function (declNode, visitArgs) {
21 visitArgs.visitDeeper = false;
22 };
23 ExtendFinderVisitor.prototype.visitMixinDefinition = function (mixinDefinitionNode, visitArgs) {
24 visitArgs.visitDeeper = false;
25 };
26 ExtendFinderVisitor.prototype.visitRuleset = function (rulesetNode, visitArgs) {
27 if (rulesetNode.root) {
28 return;
29 }
30 var i;
31 var j;
32 var extend;
33 var allSelectorsExtendList = [];
34 var extendList;
35 // get &:extend(.a); rules which apply to all selectors in this ruleset
36 var rules = rulesetNode.rules, ruleCnt = rules ? rules.length : 0;
37 for (i = 0; i < ruleCnt; i++) {
38 if (rulesetNode.rules[i] instanceof tree_1.default.Extend) {
39 allSelectorsExtendList.push(rules[i]);
40 rulesetNode.extendOnEveryPath = true;
41 }
42 }
43 // now find every selector and apply the extends that apply to all extends
44 // and the ones which apply to an individual extend
45 var paths = rulesetNode.paths;
46 for (i = 0; i < paths.length; i++) {
47 var selectorPath = paths[i], selector = selectorPath[selectorPath.length - 1], selExtendList = selector.extendList;
48 extendList = selExtendList ? utils.copyArray(selExtendList).concat(allSelectorsExtendList)
49 : allSelectorsExtendList;
50 if (extendList) {
51 extendList = extendList.map(function (allSelectorsExtend) {
52 return allSelectorsExtend.clone();
53 });
54 }
55 for (j = 0; j < extendList.length; j++) {
56 this.foundExtends = true;
57 extend = extendList[j];
58 extend.findSelfSelectors(selectorPath);
59 extend.ruleset = rulesetNode;
60 if (j === 0) {
61 extend.firstExtendOnThisSelectorPath = true;
62 }
63 this.allExtendsStack[this.allExtendsStack.length - 1].push(extend);
64 }
65 }
66 this.contexts.push(rulesetNode.selectors);
67 };
68 ExtendFinderVisitor.prototype.visitRulesetOut = function (rulesetNode) {
69 if (!rulesetNode.root) {
70 this.contexts.length = this.contexts.length - 1;
71 }
72 };
73 ExtendFinderVisitor.prototype.visitMedia = function (mediaNode, visitArgs) {
74 mediaNode.allExtends = [];
75 this.allExtendsStack.push(mediaNode.allExtends);
76 };
77 ExtendFinderVisitor.prototype.visitMediaOut = function (mediaNode) {
78 this.allExtendsStack.length = this.allExtendsStack.length - 1;
79 };
80 ExtendFinderVisitor.prototype.visitAtRule = function (atRuleNode, visitArgs) {
81 atRuleNode.allExtends = [];
82 this.allExtendsStack.push(atRuleNode.allExtends);
83 };
84 ExtendFinderVisitor.prototype.visitAtRuleOut = function (atRuleNode) {
85 this.allExtendsStack.length = this.allExtendsStack.length - 1;
86 };
87 return ExtendFinderVisitor;
88}());
89var ProcessExtendsVisitor = /** @class */ (function () {
90 function ProcessExtendsVisitor() {
91 this._visitor = new visitor_1.default(this);
92 }
93 ProcessExtendsVisitor.prototype.run = function (root) {
94 var extendFinder = new ExtendFinderVisitor();
95 this.extendIndices = {};
96 extendFinder.run(root);
97 if (!extendFinder.foundExtends) {
98 return root;
99 }
100 root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends));
101 this.allExtendsStack = [root.allExtends];
102 var newRoot = this._visitor.visit(root);
103 this.checkExtendsForNonMatched(root.allExtends);
104 return newRoot;
105 };
106 ProcessExtendsVisitor.prototype.checkExtendsForNonMatched = function (extendList) {
107 var indices = this.extendIndices;
108 extendList.filter(function (extend) {
109 return !extend.hasFoundMatches && extend.parent_ids.length == 1;
110 }).forEach(function (extend) {
111 var selector = '_unknown_';
112 try {
113 selector = extend.selector.toCSS({});
114 }
115 catch (_) { }
116 if (!indices[extend.index + " " + selector]) {
117 indices[extend.index + " " + selector] = true;
118 logger_1.default.warn("extend '" + selector + "' has no matches");
119 }
120 });
121 };
122 ProcessExtendsVisitor.prototype.doExtendChaining = function (extendsList, extendsListTarget, iterationCount) {
123 //
124 // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering
125 // and pasting the selector we would do normally, but we are also adding an extend with the same target selector
126 // this means this new extend can then go and alter other extends
127 //
128 // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors
129 // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already
130 // processed if we look at each selector at a time, as is done in visitRuleset
131 var extendIndex;
132 var targetExtendIndex;
133 var matches;
134 var extendsToAdd = [];
135 var newSelector;
136 var extendVisitor = this;
137 var selectorPath;
138 var extend;
139 var targetExtend;
140 var newExtend;
141 iterationCount = iterationCount || 0;
142 // loop through comparing every extend with every target extend.
143 // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place
144 // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one
145 // and the second is the target.
146 // the separation into two lists allows us to process a subset of chains with a bigger set, as is the
147 // case when processing media queries
148 for (extendIndex = 0; extendIndex < extendsList.length; extendIndex++) {
149 for (targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++) {
150 extend = extendsList[extendIndex];
151 targetExtend = extendsListTarget[targetExtendIndex];
152 // look for circular references
153 if (extend.parent_ids.indexOf(targetExtend.object_id) >= 0) {
154 continue;
155 }
156 // find a match in the target extends self selector (the bit before :extend)
157 selectorPath = [targetExtend.selfSelectors[0]];
158 matches = extendVisitor.findMatch(extend, selectorPath);
159 if (matches.length) {
160 extend.hasFoundMatches = true;
161 // we found a match, so for each self selector..
162 extend.selfSelectors.forEach(function (selfSelector) {
163 var info = targetExtend.visibilityInfo();
164 // process the extend as usual
165 newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector, extend.isVisible());
166 // but now we create a new extend from it
167 newExtend = new (tree_1.default.Extend)(targetExtend.selector, targetExtend.option, 0, targetExtend.fileInfo(), info);
168 newExtend.selfSelectors = newSelector;
169 // add the extend onto the list of extends for that selector
170 newSelector[newSelector.length - 1].extendList = [newExtend];
171 // record that we need to add it.
172 extendsToAdd.push(newExtend);
173 newExtend.ruleset = targetExtend.ruleset;
174 // remember its parents for circular references
175 newExtend.parent_ids = newExtend.parent_ids.concat(targetExtend.parent_ids, extend.parent_ids);
176 // only process the selector once.. if we have :extend(.a,.b) then multiple
177 // extends will look at the same selector path, so when extending
178 // we know that any others will be duplicates in terms of what is added to the css
179 if (targetExtend.firstExtendOnThisSelectorPath) {
180 newExtend.firstExtendOnThisSelectorPath = true;
181 targetExtend.ruleset.paths.push(newSelector);
182 }
183 });
184 }
185 }
186 }
187 if (extendsToAdd.length) {
188 // try to detect circular references to stop a stack overflow.
189 // may no longer be needed.
190 this.extendChainCount++;
191 if (iterationCount > 100) {
192 var selectorOne = '{unable to calculate}';
193 var selectorTwo = '{unable to calculate}';
194 try {
195 selectorOne = extendsToAdd[0].selfSelectors[0].toCSS();
196 selectorTwo = extendsToAdd[0].selector.toCSS();
197 }
198 catch (e) { }
199 throw { message: "extend circular reference detected. One of the circular extends is currently:" + selectorOne + ":extend(" + selectorTwo + ")" };
200 }
201 // now process the new extends on the existing rules so that we can handle a extending b extending c extending
202 // d extending e...
203 return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount + 1));
204 }
205 else {
206 return extendsToAdd;
207 }
208 };
209 ProcessExtendsVisitor.prototype.visitDeclaration = function (ruleNode, visitArgs) {
210 visitArgs.visitDeeper = false;
211 };
212 ProcessExtendsVisitor.prototype.visitMixinDefinition = function (mixinDefinitionNode, visitArgs) {
213 visitArgs.visitDeeper = false;
214 };
215 ProcessExtendsVisitor.prototype.visitSelector = function (selectorNode, visitArgs) {
216 visitArgs.visitDeeper = false;
217 };
218 ProcessExtendsVisitor.prototype.visitRuleset = function (rulesetNode, visitArgs) {
219 if (rulesetNode.root) {
220 return;
221 }
222 var matches;
223 var pathIndex;
224 var extendIndex;
225 var allExtends = this.allExtendsStack[this.allExtendsStack.length - 1];
226 var selectorsToAdd = [];
227 var extendVisitor = this;
228 var selectorPath;
229 // look at each selector path in the ruleset, find any extend matches and then copy, find and replace
230 for (extendIndex = 0; extendIndex < allExtends.length; extendIndex++) {
231 for (pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) {
232 selectorPath = rulesetNode.paths[pathIndex];
233 // extending extends happens initially, before the main pass
234 if (rulesetNode.extendOnEveryPath) {
235 continue;
236 }
237 var extendList = selectorPath[selectorPath.length - 1].extendList;
238 if (extendList && extendList.length) {
239 continue;
240 }
241 matches = this.findMatch(allExtends[extendIndex], selectorPath);
242 if (matches.length) {
243 allExtends[extendIndex].hasFoundMatches = true;
244 allExtends[extendIndex].selfSelectors.forEach(function (selfSelector) {
245 var extendedSelectors;
246 extendedSelectors = extendVisitor.extendSelector(matches, selectorPath, selfSelector, allExtends[extendIndex].isVisible());
247 selectorsToAdd.push(extendedSelectors);
248 });
249 }
250 }
251 }
252 rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd);
253 };
254 ProcessExtendsVisitor.prototype.findMatch = function (extend, haystackSelectorPath) {
255 //
256 // look through the haystack selector path to try and find the needle - extend.selector
257 // returns an array of selector matches that can then be replaced
258 //
259 var haystackSelectorIndex;
260 var hackstackSelector;
261 var hackstackElementIndex;
262 var haystackElement;
263 var targetCombinator;
264 var i;
265 var extendVisitor = this;
266 var needleElements = extend.selector.elements;
267 var potentialMatches = [];
268 var potentialMatch;
269 var matches = [];
270 // loop through the haystack elements
271 for (haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) {
272 hackstackSelector = haystackSelectorPath[haystackSelectorIndex];
273 for (hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) {
274 haystackElement = hackstackSelector.elements[hackstackElementIndex];
275 // if we allow elements before our match we can add a potential match every time. otherwise only at the first element.
276 if (extend.allowBefore || (haystackSelectorIndex === 0 && hackstackElementIndex === 0)) {
277 potentialMatches.push({ pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0,
278 initialCombinator: haystackElement.combinator });
279 }
280 for (i = 0; i < potentialMatches.length; i++) {
281 potentialMatch = potentialMatches[i];
282 // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't
283 // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to
284 // work out what the resulting combinator will be
285 targetCombinator = haystackElement.combinator.value;
286 if (targetCombinator === '' && hackstackElementIndex === 0) {
287 targetCombinator = ' ';
288 }
289 // if we don't match, null our match to indicate failure
290 if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) ||
291 (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) {
292 potentialMatch = null;
293 }
294 else {
295 potentialMatch.matched++;
296 }
297 // if we are still valid and have finished, test whether we have elements after and whether these are allowed
298 if (potentialMatch) {
299 potentialMatch.finished = potentialMatch.matched === needleElements.length;
300 if (potentialMatch.finished &&
301 (!extend.allowAfter &&
302 (hackstackElementIndex + 1 < hackstackSelector.elements.length || haystackSelectorIndex + 1 < haystackSelectorPath.length))) {
303 potentialMatch = null;
304 }
305 }
306 // if null we remove, if not, we are still valid, so either push as a valid match or continue
307 if (potentialMatch) {
308 if (potentialMatch.finished) {
309 potentialMatch.length = needleElements.length;
310 potentialMatch.endPathIndex = haystackSelectorIndex;
311 potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match
312 potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again
313 matches.push(potentialMatch);
314 }
315 }
316 else {
317 potentialMatches.splice(i, 1);
318 i--;
319 }
320 }
321 }
322 }
323 return matches;
324 };
325 ProcessExtendsVisitor.prototype.isElementValuesEqual = function (elementValue1, elementValue2) {
326 if (typeof elementValue1 === 'string' || typeof elementValue2 === 'string') {
327 return elementValue1 === elementValue2;
328 }
329 if (elementValue1 instanceof tree_1.default.Attribute) {
330 if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) {
331 return false;
332 }
333 if (!elementValue1.value || !elementValue2.value) {
334 if (elementValue1.value || elementValue2.value) {
335 return false;
336 }
337 return true;
338 }
339 elementValue1 = elementValue1.value.value || elementValue1.value;
340 elementValue2 = elementValue2.value.value || elementValue2.value;
341 return elementValue1 === elementValue2;
342 }
343 elementValue1 = elementValue1.value;
344 elementValue2 = elementValue2.value;
345 if (elementValue1 instanceof tree_1.default.Selector) {
346 if (!(elementValue2 instanceof tree_1.default.Selector) || elementValue1.elements.length !== elementValue2.elements.length) {
347 return false;
348 }
349 for (var i = 0; i < elementValue1.elements.length; i++) {
350 if (elementValue1.elements[i].combinator.value !== elementValue2.elements[i].combinator.value) {
351 if (i !== 0 || (elementValue1.elements[i].combinator.value || ' ') !== (elementValue2.elements[i].combinator.value || ' ')) {
352 return false;
353 }
354 }
355 if (!this.isElementValuesEqual(elementValue1.elements[i].value, elementValue2.elements[i].value)) {
356 return false;
357 }
358 }
359 return true;
360 }
361 return false;
362 };
363 ProcessExtendsVisitor.prototype.extendSelector = function (matches, selectorPath, replacementSelector, isVisible) {
364 // for a set of matches, replace each match with the replacement selector
365 var currentSelectorPathIndex = 0, currentSelectorPathElementIndex = 0, path = [], matchIndex, selector, firstElement, match, newElements;
366 for (matchIndex = 0; matchIndex < matches.length; matchIndex++) {
367 match = matches[matchIndex];
368 selector = selectorPath[match.pathIndex];
369 firstElement = new tree_1.default.Element(match.initialCombinator, replacementSelector.elements[0].value, replacementSelector.elements[0].isVariable, replacementSelector.elements[0].getIndex(), replacementSelector.elements[0].fileInfo());
370 if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) {
371 path[path.length - 1].elements = path[path.length - 1]
372 .elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));
373 currentSelectorPathElementIndex = 0;
374 currentSelectorPathIndex++;
375 }
376 newElements = selector.elements
377 .slice(currentSelectorPathElementIndex, match.index)
378 .concat([firstElement])
379 .concat(replacementSelector.elements.slice(1));
380 if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) {
381 path[path.length - 1].elements =
382 path[path.length - 1].elements.concat(newElements);
383 }
384 else {
385 path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex));
386 path.push(new tree_1.default.Selector(newElements));
387 }
388 currentSelectorPathIndex = match.endPathIndex;
389 currentSelectorPathElementIndex = match.endPathElementIndex;
390 if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) {
391 currentSelectorPathElementIndex = 0;
392 currentSelectorPathIndex++;
393 }
394 }
395 if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) {
396 path[path.length - 1].elements = path[path.length - 1]
397 .elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));
398 currentSelectorPathIndex++;
399 }
400 path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length));
401 path = path.map(function (currentValue) {
402 // we can re-use elements here, because the visibility property matters only for selectors
403 var derived = currentValue.createDerived(currentValue.elements);
404 if (isVisible) {
405 derived.ensureVisibility();
406 }
407 else {
408 derived.ensureInvisibility();
409 }
410 return derived;
411 });
412 return path;
413 };
414 ProcessExtendsVisitor.prototype.visitMedia = function (mediaNode, visitArgs) {
415 var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length - 1]);
416 newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends));
417 this.allExtendsStack.push(newAllExtends);
418 };
419 ProcessExtendsVisitor.prototype.visitMediaOut = function (mediaNode) {
420 var lastIndex = this.allExtendsStack.length - 1;
421 this.allExtendsStack.length = lastIndex;
422 };
423 ProcessExtendsVisitor.prototype.visitAtRule = function (atRuleNode, visitArgs) {
424 var newAllExtends = atRuleNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length - 1]);
425 newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, atRuleNode.allExtends));
426 this.allExtendsStack.push(newAllExtends);
427 };
428 ProcessExtendsVisitor.prototype.visitAtRuleOut = function (atRuleNode) {
429 var lastIndex = this.allExtendsStack.length - 1;
430 this.allExtendsStack.length = lastIndex;
431 };
432 return ProcessExtendsVisitor;
433}());
434exports.default = ProcessExtendsVisitor;
435//# sourceMappingURL=extend-visitor.js.map
\No newline at end of file