|
45 | 45 | extend = extendList[j];
|
46 | 46 | extend.findSelfSelectors(selectorPath);
|
47 | 47 | extend.ruleset = rulesetNode;
|
| 48 | + if (j === 0) { extend.firstExtendOnThisSelectorPath = true; } |
48 | 49 | this.allExtendsStack[this.allExtendsStack.length-1].push(extend);
|
49 | 50 | }
|
50 | 51 | }
|
|
86 | 87 | return this._visitor.visit(root);
|
87 | 88 | },
|
88 | 89 | doExtendChaining: function (extendsList, extendsListTarget, iterationCount) {
|
89 |
| - var extendIndex, targetExtendIndex, matches, extendsToAdd = [], newSelector, extendVisitor = this, selectorPath; |
| 90 | + // |
| 91 | + // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting |
| 92 | + // the selector we would do normally, but we are also adding an extend with the same target selector |
| 93 | + // this means this new extend can then go and alter other extends |
| 94 | + // |
| 95 | + // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors |
| 96 | + // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if |
| 97 | + // we look at each selector at a time, as is done in visitRuleset |
| 98 | + |
| 99 | + var extendIndex, targetExtendIndex, matches, extendsToAdd = [], newSelector, extendVisitor = this, selectorPath, extend, targetExtend; |
90 | 100 |
|
91 | 101 | iterationCount = iterationCount || 0;
|
92 | 102 |
|
| 103 | + //loop through comparing every extend with every target extend. |
| 104 | + // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place |
| 105 | + // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one |
| 106 | + // and the second is the target. |
| 107 | + // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the |
| 108 | + // case when processing media queries |
93 | 109 | for(extendIndex = 0; extendIndex < extendsList.length; extendIndex++){
|
94 | 110 | for(targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++){
|
95 | 111 |
|
96 |
| - var extend = extendsList[extendIndex]; |
97 |
| - var targetExtend = extendsListTarget[targetExtendIndex]; |
| 112 | + extend = extendsList[extendIndex]; |
| 113 | + targetExtend = extendsListTarget[targetExtendIndex]; |
| 114 | + |
| 115 | + // look for circular references |
98 | 116 | if (this.inInheritanceChain(targetExtend, extend)) { continue; }
|
99 | 117 |
|
| 118 | + // find a match in the target extends self selector (the bit before :extend) |
100 | 119 | selectorPath = [targetExtend.selfSelectors[0]];
|
101 | 120 | matches = extendVisitor.findMatch(extend, selectorPath);
|
102 | 121 |
|
103 | 122 | if (matches.length) {
|
104 | 123 |
|
| 124 | + // we found a match, so for each self selector.. |
105 | 125 | extend.selfSelectors.forEach(function(selfSelector) {
|
| 126 | + |
| 127 | + // process the extend as usual |
106 | 128 | newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector);
|
| 129 | + |
| 130 | + // but now we create a new extend from it |
107 | 131 | newExtend = new(tree.Extend)(targetExtend.selector, targetExtend.option, 0);
|
108 | 132 | newExtend.selfSelectors = newSelector;
|
| 133 | + |
| 134 | + // add the extend onto the list of extends for that selector |
109 | 135 | newSelector[newSelector.length-1].extendList = [newExtend];
|
| 136 | + |
| 137 | + // record that we need to add it. |
110 | 138 | extendsToAdd.push(newExtend);
|
111 | 139 | newExtend.ruleset = targetExtend.ruleset;
|
| 140 | + |
| 141 | + //remember its parents for circular references |
112 | 142 | newExtend.parents = [targetExtend, extend];
|
113 |
| - targetExtend.ruleset.paths.push(newSelector); |
| 143 | + |
| 144 | + // only process the selector once.. if we have :extend(.a,.b) then multiple |
| 145 | + // extends will look at the same selector path, so when extending |
| 146 | + // we know that any others will be duplicates in terms of what is added to the css |
| 147 | + if (targetExtend.firstExtendOnThisSelectorPath) { |
| 148 | + newExtend.firstExtendOnThisSelectorPath = true; |
| 149 | + targetExtend.ruleset.paths.push(newSelector); |
| 150 | + } |
114 | 151 | });
|
115 | 152 | }
|
116 | 153 | }
|
117 | 154 | }
|
118 | 155 |
|
119 | 156 | if (extendsToAdd.length) {
|
| 157 | + // try to detect circular references to stop a stack overflow. |
| 158 | + // may no longer be needed. |
120 | 159 | this.extendChainCount++;
|
121 | 160 | if (iterationCount > 100) {
|
122 | 161 | var selectorOne = "{unable to calculate}";
|
|
129 | 168 | catch(e) {}
|
130 | 169 | throw {message: "extend circular reference detected. One of the circular extends is currently:"+selectorOne+":extend(" + selectorTwo+")"};
|
131 | 170 | }
|
| 171 | + |
| 172 | + // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e... |
132 | 173 | return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount+1));
|
133 | 174 | } else {
|
134 | 175 | return extendsToAdd;
|
|
308 | 349 | visitRulesetOut: function (rulesetNode) {
|
309 | 350 | },
|
310 | 351 | visitMedia: function (mediaNode, visitArgs) {
|
311 |
| - this.allExtendsStack.push(mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1])); |
| 352 | + var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]); |
| 353 | + newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends)); |
| 354 | + this.allExtendsStack.push(newAllExtends); |
312 | 355 | },
|
313 | 356 | visitMediaOut: function (mediaNode) {
|
314 | 357 | this.allExtendsStack.length = this.allExtendsStack.length - 1;
|
315 | 358 | },
|
316 | 359 | visitDirective: function (directiveNode, visitArgs) {
|
317 |
| - this.allExtendsStack.push(directiveNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1])); |
| 360 | + var newAllExtends = directiveNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]); |
| 361 | + newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, directiveNode.allExtends)); |
| 362 | + this.allExtendsStack.push(newAllExtends); |
318 | 363 | },
|
319 | 364 | visitDirectiveOut: function (directiveNode) {
|
320 | 365 | this.allExtendsStack.length = this.allExtendsStack.length - 1;
|
|
0 commit comments