diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index daa3b7162..46174120d 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -867,7 +867,8 @@ var Parser = function Parser(context, imports, fileInfo) { e = parserInput.$re(/^(?:\d+\.\d+|\d+)%/) || parserInput.$re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) || parserInput.$char('*') || parserInput.$char('&') || this.attribute() || - parserInput.$re(/^\([^()@]+\)/) || parserInput.$re(/^[\.#:](?=@)/) || this.entities.variableCurly(); + parserInput.$re(/^\([^&()@]+\)/) || parserInput.$re(/^[\.#:](?=@)/) || + this.entities.variableCurly(); if (! e) { parserInput.save(); diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index 2c1bb8a4d..f1bfc4aae 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -2,6 +2,7 @@ var Node = require("./node"), Rule = require("./rule"), Selector = require("./selector"), Element = require("./element"), + Paren = require("./paren"), contexts = require("../contexts"), defaultFunc = require("../functions/default"), getDebugInfo = require("./debug-info"); @@ -462,175 +463,245 @@ Ruleset.prototype.joinSelectors = function (paths, context, selectors) { Ruleset.prototype.joinSelector = function (paths, context, selector) { - var i, j, k, - hasParentSelector, newSelectors, el, sel, parentSel, - newSelectorPath, afterParentJoin, newJoinedSelector, - newJoinedSelectorEmpty, lastSelector, currentElements, - selectorsMultiplied; - - for (i = 0; i < selector.elements.length; i++) { - el = selector.elements[i]; - if (el.value === '&') { - hasParentSelector = true; - } - } - - if (!hasParentSelector) { - if (context.length > 0) { - for (i = 0; i < context.length; i++) { - paths.push(context[i].concat(selector)); + function createParenthesis(elementsToPak, originalElement) { + var replacementParen, j; + if (elementsToPak.length === 0) { + replacementParen = new Paren(elementsToPak[0]); + } else { + var insideParent = []; + for (j = 0; j < elementsToPak.length; j++) { + insideParent.push(new Element(null, elementsToPak[j], originalElement.index, originalElement.currentFileInfo)); } + replacementParen = new Paren(new Selector(insideParent)); } - else { - paths.push([selector]); - } - return; - } - - // The paths are [[Selector]] - // The first list is a list of comma separated selectors - // The inner list is a list of inheritance separated selectors - // e.g. - // .a, .b { - // .c { - // } - // } - // == [[.a] [.c]] [[.b] [.c]] - // - - // the elements from the current selector so far - currentElements = []; - // the current list of new selectors to add to the path. - // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors - // by the parents - newSelectors = [[]]; - - for (i = 0; i < selector.elements.length; i++) { - el = selector.elements[i]; - // non parent reference elements just get added - if (el.value !== "&") { - currentElements.push(el); - } else { - // the new list of selectors to add - selectorsMultiplied = []; + return replacementParen; + } + + function createSelector(containedElement, originalElement) { + var element, selector; + element = new Element(null, containedElement, originalElement.index, originalElement.currentFileInfo); + selector = new Selector([element]); + return selector; + } + + // replace all parent selectors inside `inSelector` by content of `context` array + // resulting selectors are returned inside `paths` array + // returns true if `inSelector` contained at least one parent selector + function replaceParentSelector(paths, context, inSelector) { + // The paths are [[Selector]] + // The first list is a list of comma separated selectors + // The inner list is a list of inheritance separated selectors + // e.g. + // .a, .b { + // .c { + // } + // } + // == [[.a] [.c]] [[.b] [.c]] + // + var i, j, k, currentElements, newSelectors, selectorsMultiplied, sel, el, hadParentSelector = false; + function findNestedSelector(element) { + var maybeSelector; + if (element.value.type !== 'Paren') { + return null; + } - // merge the current list of non parent selector elements - // on to the current list of selectors to add - if (currentElements.length > 0) { - this.mergeElementsOnToSelectors(currentElements, newSelectors); + maybeSelector = element.value.value; + if (maybeSelector.type !== 'Selector') { + return null; } - // loop through our current selectors - for (j = 0; j < newSelectors.length; j++) { - sel = newSelectors[j]; - // if we don't have any parent paths, the & might be in a mixin so that it can be used - // whether there are parents or not - if (context.length === 0) { - // the combinator used on el should now be applied to the next element instead so that - // it is not lost - if (sel.length > 0) { - sel[0].elements = sel[0].elements.slice(0); - sel[0].elements.push(new Element(el.combinator, '', el.index, el.currentFileInfo)); + return maybeSelector; + } + + // the elements from the current selector so far + currentElements = []; + // the current list of new selectors to add to the path. + // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors + // by the parents + newSelectors = [ + [] + ]; + + for (i = 0; i < inSelector.elements.length; i++) { + el = inSelector.elements[i]; + // non parent reference elements just get added + if (el.value !== "&") { + var nestedSelector = findNestedSelector(el); + if (nestedSelector != null) { + // merge the current list of non parent selector elements + // on to the current list of selectors to add + mergeElementsOnToSelectors(currentElements, newSelectors); + + var nestedPaths = [], replaced, replacedNewSelectors = []; + replaced = replaceParentSelector(nestedPaths, context, nestedSelector); + hadParentSelector = hadParentSelector || replaced; + //the nestedPaths array should have only one member - replaceParentSelector does not multiply selectors + for (k = 0; k < nestedPaths.length; k++) { + var replacementSelector = createSelector(createParenthesis(nestedPaths[k], el), el); + addAllReplacementsIntoPath(newSelectors, [replacementSelector], el, inSelector, replacedNewSelectors); } - selectorsMultiplied.push(sel); + newSelectors = replacedNewSelectors; + currentElements = []; + + } else { + currentElements.push(el); } - else { - // and the parent selectors - for (k = 0; k < context.length; k++) { - parentSel = context[k]; - // We need to put the current selectors - // then join the last selector's elements on to the parents selectors - - // our new selector path - newSelectorPath = []; - // selectors from the parent after the join - afterParentJoin = []; - newJoinedSelectorEmpty = true; - - //construct the joined selector - if & is the first thing this will be empty, - // if not newJoinedSelector will be the last set of elements in the selector + + } else { + hadParentSelector = true; + // the new list of selectors to add + selectorsMultiplied = []; + + // merge the current list of non parent selector elements + // on to the current list of selectors to add + mergeElementsOnToSelectors(currentElements, newSelectors); + + // loop through our current selectors + for (j = 0; j < newSelectors.length; j++) { + sel = newSelectors[j]; + // if we don't have any parent paths, the & might be in a mixin so that it can be used + // whether there are parents or not + if (context.length === 0) { + // the combinator used on el should now be applied to the next element instead so that + // it is not lost if (sel.length > 0) { - newSelectorPath = sel.slice(0); - lastSelector = newSelectorPath.pop(); - newJoinedSelector = selector.createDerived(lastSelector.elements.slice(0)); - newJoinedSelectorEmpty = false; + sel[0].elements.push(new Element(el.combinator, '', el.index, el.currentFileInfo)); } - else { - newJoinedSelector = selector.createDerived([]); + selectorsMultiplied.push(sel); + } + else { + // and the parent selectors + for (k = 0; k < context.length; k++) { + // We need to put the current selectors + // then join the last selector's elements on to the parents selectors + var newSelectorPath = addReplacementIntoPath(sel, context[k], el, inSelector); + // add that to our new set of selectors + selectorsMultiplied.push(newSelectorPath); } + } + } - //put together the parent selectors after the join - if (parentSel.length > 1) { - afterParentJoin = afterParentJoin.concat(parentSel.slice(1)); - } + // our new selectors has been multiplied, so reset the state + newSelectors = selectorsMultiplied; + currentElements = []; + } + } - if (parentSel.length > 0) { - newJoinedSelectorEmpty = false; - - // /deep/ is a combinator that is valid without anything in front of it - // so if the & does not have a combinator that is "" or " " then - // and there is a combinator on the parent, then grab that. - // this also allows + a { & .b { .a & { ... though not sure why you would want to do that - var combinator = el.combinator, - parentEl = parentSel[0].elements[0]; - if (combinator.emptyOrWhitespace && !parentEl.combinator.emptyOrWhitespace) { - combinator = parentEl.combinator; - } - // join the elements so far with the first part of the parent - newJoinedSelector.elements.push(new Element(combinator, parentEl.value, el.index, el.currentFileInfo)); - newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1)); - } + // if we have any elements left over (e.g. .a& .b == .b) + // add them on to all the current selectors + mergeElementsOnToSelectors(currentElements, newSelectors); - if (!newJoinedSelectorEmpty) { - // now add the joined selector - newSelectorPath.push(newJoinedSelector); - } + for (i = 0; i < newSelectors.length; i++) { + if (newSelectors[i].length > 0) { + paths.push(newSelectors[i]); + } + } + + return hadParentSelector; + } - // and the rest of the parent - newSelectorPath = newSelectorPath.concat(afterParentJoin); + // joins selector path from `beginningPath` with selector path in `addPath` + // `replacedElement` contains element that is being replaced by `addPath` + // returns concatenated path + function addReplacementIntoPath(beginningPath, addPath, replacedElement, originalSelector) { + var newSelectorPath, lastSelector, newJoinedSelector; + // our new selector path + newSelectorPath = []; - // add that to our new set of selectors - selectorsMultiplied.push(newSelectorPath); - } - } + //construct the joined selector - if & is the first thing this will be empty, + // if not newJoinedSelector will be the last set of elements in the selector + if (beginningPath.length > 0) { + newSelectorPath = beginningPath.slice(0); + lastSelector = newSelectorPath.pop(); + newJoinedSelector = originalSelector.createDerived(lastSelector.elements.slice(0)); + } + else { + newJoinedSelector = originalSelector.createDerived([]); + } + + if (addPath.length > 0) { + // /deep/ is a combinator that is valid without anything in front of it + // so if the & does not have a combinator that is "" or " " then + // and there is a combinator on the parent, then grab that. + // this also allows + a { & .b { .a & { ... though not sure why you would want to do that + var combinator = replacedElement.combinator, parentEl = addPath[0].elements[0]; + if (combinator.emptyOrWhitespace && !parentEl.combinator.emptyOrWhitespace) { + combinator = parentEl.combinator; } + // join the elements so far with the first part of the parent + newJoinedSelector.elements.push(new Element(combinator, parentEl.value, replacedElement.index, replacedElement.currentFileInfo)); + newJoinedSelector.elements = newJoinedSelector.elements.concat(addPath[0].elements.slice(1)); + } - // our new selectors has been multiplied, so reset the state - newSelectors = selectorsMultiplied; - currentElements = []; + // now add the joined selector - but only if it is not empty + if (newJoinedSelector.elements.length !== 0) { + newSelectorPath.push(newJoinedSelector); } - } - // if we have any elements left over (e.g. .a& .b == .b) - // add them on to all the current selectors - if (currentElements.length > 0) { - this.mergeElementsOnToSelectors(currentElements, newSelectors); + //put together the parent selectors after the join (e.g. the rest of the parent) + if (addPath.length > 1) { + newSelectorPath = newSelectorPath.concat(addPath.slice(1)); + } + return newSelectorPath; } - for (i = 0; i < newSelectors.length; i++) { - if (newSelectors[i].length > 0) { - paths.push(newSelectors[i]); + // joins selector path from `beginningPath` with every selector path in `addPaths` array + // `replacedElement` contains element that is being replaced by `addPath` + // returns array with all concatenated paths + function addAllReplacementsIntoPath( beginningPath, addPaths, replacedElement, originalSelector, result) { + var j; + for (j = 0; j < beginningPath.length; j++) { + var newSelectorPath = addReplacementIntoPath(beginningPath[j], addPaths, replacedElement, originalSelector); + result.push(newSelectorPath); } + return result; } -}; -Ruleset.prototype.mergeElementsOnToSelectors = function(elements, selectors) { - var i, sel; - if (selectors.length === 0) { - selectors.push([ new Selector(elements) ]); - return; + function mergeElementsOnToSelectors(elements, selectors) { + var i, sel; + + if (elements.length === 0) { + return ; + } + if (selectors.length === 0) { + selectors.push([ new Selector(elements) ]); + return; + } + + for (i = 0; i < selectors.length; i++) { + sel = selectors[i]; + + // if the previous thing in sel is a parent this needs to join on to it + if (sel.length > 0) { + sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements)); + } + else { + sel.push(new Selector(elements)); + } + } } - for (i = 0; i < selectors.length; i++) { - sel = selectors[i]; + // joinSelector code follows + var i, newPaths, hadParentSelector; + + newPaths = []; + hadParentSelector = replaceParentSelector(newPaths, context, selector); - // if the previous thing in sel is a parent this needs to join on to it - if (sel.length > 0) { - sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements)); + if (!hadParentSelector) { + if (context.length > 0) { + newPaths = []; + for (i = 0; i < context.length; i++) { + newPaths.push(context[i].concat(selector)); + } } else { - sel.push(new Selector(elements)); + newPaths = [[selector]]; } } + + for (i = 0; i < newPaths.length; i++) { + paths.push(newPaths[i]); + } + }; module.exports = Ruleset; diff --git a/test/css/selectors.css b/test/css/selectors.css index ed3703593..672a8d547 100644 --- a/test/css/selectors.css +++ b/test/css/selectors.css @@ -154,3 +154,12 @@ blank blank blank blank blank blank blank blank blank blank blank blank blank bl .blood { color: red; } +.foo:not(.tst.only-nested .level2:hover) { + test: only-nested; +} +.foo.nestend-and-non-nested:not(.tst.nestend-and-non-nested:hover) { + test: nestend-and-non-nested; +} +.selector:not(:hover) { + test: global scope; +} diff --git a/test/less/selectors.less b/test/less/selectors.less index 2680fbac9..533db8f75 100644 --- a/test/less/selectors.less +++ b/test/less/selectors.less @@ -123,7 +123,7 @@ a { } @num: 3; :nth-child(@{num}) { - selector: interpolated; + selector: interpolated; } .test { &:nth-child(@{num}) { @@ -132,7 +132,7 @@ a { &:nth-child(odd):not(:nth-child(3)) { color: #ff0000; } -} + } [prop], [prop=10%], [prop="value@{num}"], @@ -156,4 +156,19 @@ blank blank blank blank blank blank blank blank blank blank blank blank blank bl */ @{selector} { color: red; -} \ No newline at end of file +} +.only-nested { + .level2 { + .foo:not(.tst&:hover) { + test: only-nested; + } + } +} +.nestend-and-non-nested { + .foo&:not(.tst&:hover) { + test: nestend-and-non-nested; + } +} +.selector:not(&:hover) { + test: global scope; +}