diff --git a/lib/less/extend-visitor.js b/lib/less/extend-visitor.js index 6cbdbcb5..d5c8ecc8 100644 --- a/lib/less/extend-visitor.js +++ b/lib/less/extend-visitor.js @@ -95,112 +95,146 @@ if (rulesetNode.root) { return; } - var i, j, k, selector, element, allExtends = this.allExtendsStack[this.allExtendsStack.length-1], selectorsToAdd = []; + var matches, pathIndex, extendIndex, allExtends = this.allExtendsStack[this.allExtendsStack.length-1], selectorsToAdd = [], extendVisitor = this; + + // 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]; + matches = this.findMatch(allExtends[extendIndex], selectorPath); - for(k = 0; k < allExtends.length; k++) { - for(i = 0; i < rulesetNode.paths.length; i++) { - selectorPath = rulesetNode.paths[i]; - var matches = this.findMatch(allExtends[k], selectorPath); if (matches.length) { - allExtends[k].selfSelectors.forEach(function(selfSelector) { - var currentSelectorPathIndex = 0, - currentSelectorPathElementIndex = 0, - path = []; - for(j = 0; j < matches.length; j++) { - match = matches[j]; - var selector = selectorPath[match.pathIndex], - firstElement = new tree.Element( - match.initialCombinator, - selfSelector.elements[0].value, - selfSelector.elements[0].index - ); - 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++; - } - - path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex)); - - path.push(new tree.Selector( - selector.elements - .slice(currentSelectorPathElementIndex, match.index) - .concat([firstElement]) - .concat(selfSelector.elements.slice(1)) - )); - currentSelectorPathIndex = match.endPathIndex; - currentSelectorPathElementIndex = match.endPathElementIndex; - if (currentSelectorPathElementIndex >= selector.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)); - currentSelectorPathElementIndex = 0; - currentSelectorPathIndex++; - } - - path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length)); - - selectorsToAdd.push(path); + allExtends[extendIndex].selfSelectors.forEach(function(selfSelector) { + selectorsToAdd.push(extendVisitor.extendSelector(matches, selfSelector)); }); } } } rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd); }, - findMatch: function (extend, selectorPath) { - var i, k, l, element, hasMatch, potentialMatches = [], potentialMatch, matches = []; - for(k = 0; k < selectorPath.length; k++) { - selector = selectorPath[k]; - for(i = 0; i < selector.elements.length; i++) { - if (extend.allowBefore || (k == 0 && i == 0)) { - potentialMatches.push({pathIndex: k, index: i, matched: 0, initialCombinator: selector.elements[i].combinator}); + 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, hackstackSelector, hackstackElementIndex, haystackElement, + targetCombinator, i, + needleElements = extend.selector.elements, + potentialMatches = [], potentialMatch, 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(l = 0; l < potentialMatches.length; l++) { - potentialMatch = potentialMatches[l]; + for(i = 0; i < potentialMatches.length; i++) { + potentialMatch = potentialMatches[i]; - var targetCombinator = selector.elements[i].combinator.value; - if (targetCombinator == '' && i === 0) { + // 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 (extend.selector.elements[potentialMatch.matched].value !== selector.elements[i].value || - (potentialMatch.matched > 0 && extend.selector.elements[potentialMatch.matched].combinator.value !== targetCombinator)) { + + // if we don't match, null our match to indicate failure + if (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 === extend.selector.elements.length; + potentialMatch.finished = potentialMatch.matched === needleElements.length; if (potentialMatch.finished && - (!extend.allowAfter && (i+1 < selector.elements.length || - k+1 < selectorPath.length))) { + (!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 = extend.selector.elements.length; - potentialMatch.endPathIndex = k; - potentialMatch.endPathElementIndex = i+1; // index after end of match - potentialMatches.length = 0; + 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); - break; } } else { - potentialMatches.splice(l, 1); - l--; + potentialMatches.splice(i, 1); + i--; } } } } return matches; }, + extendSelector:function (matches, replacementSelector) { + + //for a set of matches, replace each match with the replacement selector + + var currentSelectorPathIndex = 0, + currentSelectorPathElementIndex = 0, + path = [], + matchIndex, + selector, + firstElement; + + for (matchIndex = 0; matchIndex < matches.length; matchIndex++) { + match = matches[matchIndex]; + selector = selectorPath[match.pathIndex]; + firstElement = new tree.Element( + match.initialCombinator, + replacementSelector.elements[0].value, + replacementSelector.elements[0].index + ); + + 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++; + } + + path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex)); + + path.push(new tree.Selector( + selector.elements + .slice(currentSelectorPathElementIndex, match.index) + .concat([firstElement]) + .concat(replacementSelector.elements.slice(1)) + )); + currentSelectorPathIndex = match.endPathIndex; + currentSelectorPathElementIndex = match.endPathElementIndex; + if (currentSelectorPathElementIndex >= selector.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)); + currentSelectorPathElementIndex = 0; + currentSelectorPathIndex++; + } + + path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length)); + + return path; + }, visitRulesetOut: function (rulesetNode) { }, visitMedia: function (mediaNode, visitArgs) {