diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index 2e631c15f..80f7c525e 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -814,7 +814,7 @@ var Parser = function Parser(context, imports, fileInfo) { if (!e) { break; } - elem = new(tree.Element)(c, e, elemIndex, fileInfo); + elem = new(tree.Element)(c, e, false, elemIndex, fileInfo); if (elements) { elements.push(elem); } else { @@ -1101,7 +1101,7 @@ var Parser = function Parser(context, imports, fileInfo) { } } - if (e) { return new(tree.Element)(c, e, index, fileInfo); } + if (e) { return new(tree.Element)(c, e, e instanceof tree.Variable, index, fileInfo); } }, // @@ -1181,6 +1181,30 @@ var Parser = function Parser(context, imports, fileInfo) { if (elements) { return new(tree.Selector)(elements, allExtends, condition, index, fileInfo); } if (allExtends) { error('Extend must be used to extend a selector, it cannot be used on its own'); } }, + selectors: function () { + var s, selectors; + while (true) { + s = this.selector(); + if (!s) { + break; + } + if (selectors) { + selectors.push(s); + } else { + selectors = [ s ]; + } + parserInput.commentStore.length = 0; + if (s.condition && selectors.length > 1) { + error("Guards are only currently allowed on a single selector."); + } + if (!parserInput.$char(',')) { break; } + if (s.condition) { + error("Guards are only currently allowed on a single selector."); + } + parserInput.commentStore.length = 0; + } + return selectors; + }, attribute: function () { if (!parserInput.$char('[')) { return; } @@ -1232,7 +1256,7 @@ var Parser = function Parser(context, imports, fileInfo) { // div, .class, body > p {...} // ruleset: function () { - var selectors, s, rules, debugInfo; + var selectors, rules, debugInfo; parserInput.save(); @@ -1240,26 +1264,7 @@ var Parser = function Parser(context, imports, fileInfo) { debugInfo = getDebugInfo(parserInput.i); } - while (true) { - s = this.selector(); - if (!s) { - break; - } - if (selectors) { - selectors.push(s); - } else { - selectors = [ s ]; - } - parserInput.commentStore.length = 0; - if (s.condition && selectors.length > 1) { - error('Guards are only currently allowed on a single selector.'); - } - if (!parserInput.$char(',')) { break; } - if (s.condition) { - error('Guards are only currently allowed on a single selector.'); - } - parserInput.commentStore.length = 0; - } + selectors = this.selectors(); if (selectors && (rules = this.block())) { parserInput.forget(); diff --git a/lib/less/tree/element.js b/lib/less/tree/element.js index 371d25565..13fa3b2ba 100644 --- a/lib/less/tree/element.js +++ b/lib/less/tree/element.js @@ -2,7 +2,7 @@ var Node = require('./node'), Paren = require('./paren'), Combinator = require('./combinator'); -var Element = function (combinator, value, index, currentFileInfo, visibilityInfo) { +var Element = function (combinator, value, isVariable, index, currentFileInfo, visibilityInfo) { this.combinator = combinator instanceof Combinator ? combinator : new Combinator(combinator); @@ -13,6 +13,7 @@ var Element = function (combinator, value, index, currentFileInfo, visibilityInf } else { this.value = ''; } + this.isVariable = isVariable; this._index = index; this._fileInfo = currentFileInfo; this.copyVisibilityInfo(visibilityInfo); @@ -30,12 +31,14 @@ Element.prototype.accept = function (visitor) { Element.prototype.eval = function (context) { return new Element(this.combinator, this.value.eval ? this.value.eval(context) : this.value, + this.isVariable, this.getIndex(), this.fileInfo(), this.visibilityInfo()); }; Element.prototype.clone = function () { return new Element(this.combinator, this.value, + this.isVariable, this.getIndex(), this.fileInfo(), this.visibilityInfo()); }; diff --git a/lib/less/tree/mixin-definition.js b/lib/less/tree/mixin-definition.js index b73bf5362..9b38258df 100644 --- a/lib/less/tree/mixin-definition.js +++ b/lib/less/tree/mixin-definition.js @@ -8,7 +8,7 @@ var Selector = require('./selector'), var Definition = function (name, params, rules, condition, variadic, frames, visibilityInfo) { this.name = name; - this.selectors = [new Selector([new Element(null, name, this._index, this._fileInfo)])]; + this.selectors = [new Selector([new Element(null, name, false, this._index, this._fileInfo)])]; this.params = params; this.condition = condition; this.variadic = variadic; diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index 0b83520ef..66304c663 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -41,22 +41,47 @@ Ruleset.prototype.accept = function (visitor) { } }; Ruleset.prototype.eval = function (context) { - var thisSelectors = this.selectors, selectors, - selCnt, selector, i, hasOnePassingSelector = false; + var that = this, selectors, selCnt, selector, i, hasVariable, hasOnePassingSelector = false; - if (thisSelectors && (selCnt = thisSelectors.length)) { + if (this.selectors && (selCnt = this.selectors.length)) { selectors = new Array(selCnt); defaultFunc.error({ type: 'Syntax', message: 'it is currently only allowed in parametric mixin guards,' }); + for (i = 0; i < selCnt; i++) { - selector = thisSelectors[i].eval(context); + selector = this.selectors[i].eval(context); + for (var j = 0; j < selector.elements.length; j++) { + if (selector.elements[j].isVariable) { + hasVariable = true; + break; + } + } selectors[i] = selector; if (selector.evaldCondition) { hasOnePassingSelector = true; } } + + if (hasVariable) { + var toParseSelectors = new Array(selCnt); + for (i = 0; i < selCnt; i++) { + selector = selectors[i]; + toParseSelectors[i] = selector.toCSS(context); + } + this.parse.parseNode( + toParseSelectors.join(','), + ["selectors"], + selectors[0].getIndex(), + selectors[0].fileInfo(), + function(err, result) { + if (result) { + selectors = utils.flattenArray(result); + } + }); + } + defaultFunc.reset(); } else { hasOnePassingSelector = true; @@ -162,7 +187,7 @@ Ruleset.prototype.eval = function (context) { // for rulesets, check if it is a css guard and can be removed if (rule instanceof Ruleset && rule.selectors && rule.selectors.length === 1) { // check if it can be folded in (e.g. & where) - if (rule.selectors[0].isJustParentSelector()) { + if (rule.selectors[0] && rule.selectors[0].isJustParentSelector()) { rsRules.splice(i--, 1); for (var j = 0; (subRule = rule.rules[j]); j++) { @@ -511,7 +536,13 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) { } else { var insideParent = new Array(elementsToPak.length); for (j = 0; j < elementsToPak.length; j++) { - insideParent[j] = new Element(null, elementsToPak[j], originalElement._index, originalElement._fileInfo); + insideParent[j] = new Element( + null, + elementsToPak[j], + originalElement.isVariable, + originalElement._index, + originalElement._fileInfo + ); } replacementParen = new Paren(new Selector(insideParent)); } @@ -520,7 +551,7 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) { function createSelector(containedElement, originalElement) { var element, selector; - element = new Element(null, containedElement, originalElement._index, originalElement._fileInfo); + element = new Element(null, containedElement, originalElement.isVariable, originalElement._index, originalElement._fileInfo); selector = new Selector([element]); return selector; } @@ -545,7 +576,8 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) { } if (addPath.length > 0) { - // /deep/ is a combinator that is valid without anything in front of it + // /deep/ is a CSS4 selector - (removed, so should deprecate) + // 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 @@ -554,7 +586,13 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) { 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._fileInfo)); + newJoinedSelector.elements.push(new Element( + combinator, + parentEl.value, + replacedElement.isVariable, + replacedElement._index, + replacedElement._fileInfo + )); newJoinedSelector.elements = newJoinedSelector.elements.concat(addPath[0].elements.slice(1)); } @@ -688,7 +726,7 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) { // 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.push(new Element(el.combinator, '', el._index, el._fileInfo)); + sel[0].elements.push(new Element(el.combinator, '', el.isVariable, el._index, el._fileInfo)); } selectorsMultiplied.push(sel); } diff --git a/lib/less/tree/selector.js b/lib/less/tree/selector.js index 1486049b4..18808a356 100644 --- a/lib/less/tree/selector.js +++ b/lib/less/tree/selector.js @@ -54,7 +54,7 @@ Selector.prototype.getElements = function(els) { return els; }; Selector.prototype.createEmptySelectors = function() { - var el = new Element('', '&', this._index, this._fileInfo), + var el = new Element('', '&', false, this._index, this._fileInfo), sels = [new Selector([el], null, null, this._index, this._fileInfo)]; sels[0].mediaEmpty = true; return sels; diff --git a/lib/less/utils.js b/lib/less/utils.js index 185b9a37e..1c830e7c8 100644 --- a/lib/less/utils.js +++ b/lib/less/utils.js @@ -1,5 +1,5 @@ /* jshint proto: true */ -module.exports = { +var utils = { getLocation: function(index, inputStream) { var n = index + 1, line = null, @@ -65,5 +65,21 @@ module.exports = { } } return obj1; + }, + flattenArray: function(arr, result) { + result = result || []; + for (var i = 0, length = arr.length; i < length; i++) { + var value = arr[i]; + if (Array.isArray(value)) { + utils.flattenArray(value, result); + } else { + if (value !== undefined) { + result.push(value); + } + } + } + return result; } }; + +module.exports = utils; \ No newline at end of file diff --git a/lib/less/visitors/extend-visitor.js b/lib/less/visitors/extend-visitor.js index d8f7c17f3..be13f8242 100644 --- a/lib/less/visitors/extend-visitor.js +++ b/lib/less/visitors/extend-visitor.js @@ -385,6 +385,7 @@ ProcessExtendsVisitor.prototype = { firstElement = new tree.Element( match.initialCombinator, replacementSelector.elements[0].value, + replacementSelector.elements[0].isVariable, replacementSelector.elements[0].getIndex(), replacementSelector.elements[0].fileInfo() ); diff --git a/test/css/parse-interpolation.css b/test/css/parse-interpolation.css new file mode 100644 index 000000000..21122d72e --- /dev/null +++ b/test/css/parse-interpolation.css @@ -0,0 +1,36 @@ +input[type=text]:focus, +input[type=email]:focus, +input[type=password]:focus, +textarea:focus { + foo: bar; +} +.a + .z, +.b + .z, +.c + .z { + color: blue; +} +.bar .d.a, +.bar .b, +.c.bar:hover, +.bar baz { + color: blue; +} +.a + .e, +.b.c + .e, +.d + .e { + foo: bar; +} +input[class="text"], +input.text { + background: red; +} +.master-page-1 .selector-1, +.master-page-1 .selector-2 { + background-color: red; +} +.fruit-apple, +.fruit-satsuma, +.fruit-banana, +.fruit-pear { + content: "Just a test."; +} diff --git a/test/css/permissive-parse.css b/test/css/permissive-parse.css index 1ed0773d4..f5f498afd 100644 --- a/test/css/permissive-parse.css +++ b/test/css/permissive-parse.css @@ -21,7 +21,9 @@ @-moz-whatever (foo: "(" bam ")") { bar: foo; } -#selector, .bar, foo[attr="blah"] { +#selector, +.bar, +foo[attr="blah"] { bar: value; } @media (min-width: 640px) { diff --git a/test/less/parse-interpolation.less b/test/less/parse-interpolation.less new file mode 100644 index 000000000..9c9c7afca --- /dev/null +++ b/test/less/parse-interpolation.less @@ -0,0 +1,53 @@ +@inputs: input[type=text], input[type=email], input[type=password], textarea; + +@{inputs} { + &:focus { + foo: bar; + } +} + +@classes: .a, .b, .c; + +@{classes} { + + .z { + color: blue; + } +} + +.bar { + .d@{classes}&:hover, baz { + color: blue; + } +} + +@c: ~'.a, .b'; +@d: ~'.c, .d'; +@e: ~' + .e'; + +@{c}@{d} { + @{e} { + foo: bar; + } +} + +@textClasses: ~'&[class="text"], &.text'; + +input { + @{textClasses} { + background: red; + } +} + +@my-selector: ~'.selector-1, .selector-2'; +.master-page-1 { + @{my-selector} { + background-color: red; + } +} + +@list: apple, satsuma, banana, pear; +@{list} { + .fruit-& { + content: "Just a test."; + } +} \ No newline at end of file