From 23569c98e2df6cb55867682fe86b62b9fa557591 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sat, 16 Jun 2018 11:58:09 -0700 Subject: [PATCH] Fixes #1421 - re-parses variable-interpolated elements to selectors (#3217) * Adds passing test from #3098 * Added passing test example from #1817 * Allow lists to be re-evaluated as selectors (Fixes #1694) --- lib/less/parser/parser.js | 51 +++++++++-------- lib/less/tree/element.js | 5 +- lib/less/tree/mixin-definition.js | 2 +- lib/less/tree/ruleset.js | 88 +++++++++++++++++++++++++---- lib/less/tree/selector.js | 2 +- lib/less/utils.js | 18 +++++- lib/less/visitors/extend-visitor.js | 1 + test/css/parse-interpolation.css | 21 +++++++ test/less-test.js | 2 +- test/less/parse-interpolation.less | 29 ++++++++++ 10 files changed, 179 insertions(+), 40 deletions(-) create mode 100644 test/css/parse-interpolation.css create mode 100644 test/less/parse-interpolation.less diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index ef8357ce4..611af8780 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -810,7 +810,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 { @@ -1097,7 +1097,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); } }, // @@ -1177,6 +1177,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; } @@ -1228,7 +1252,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(); @@ -1236,26 +1260,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 c759f97f0..2890dd1e8 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 31b3b39ad..b5f0119b5 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 8c2ff3956..c57facc11 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -41,20 +41,72 @@ 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, 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); - selectors[i] = selector; - if (selector.evaldCondition) { - hasOnePassingSelector = true; + selector = this.selectors[i].eval(context); + var removeSelector = false; + for (var j = 0; j < selector.elements.length; j++) { + var el = selector.elements[j]; + // If selector elements were variables, re-parse to see if they are actually selectors + if (el.isVariable) { + var selectorsToInsert; + if (Array.isArray(el.value.value)) { + // Convert var evaluated to list as list of selectors + selectorsToInsert = new Array(el.value.value.length); + el.value.value.forEach(function(val, k) { + selectorsToInsert[k] = new Selector( + [new Element( + null, + val.value, + false, + el.getIndex(), + el.fileInfo() + )] + ); + }) + } else if (typeof el.value.value === 'string') { + this.parse.parseNode( + el.value.value, + ["selectors"], + el.getIndex(), + el.fileInfo(), + function(err, result) { + el.isVariable = false; + if (result) { + result = utils.flattenArray(result); + // If the parsed element matches itself, it's still an element + if (result.length !== 1 || el.value.value !== result[0].elements[0].value) { + selectorsToInsert = result; + } + } + }); + } + if (selectorsToInsert) { + this.selectors = this.selectors.slice(0, i + 1) + .concat(selectorsToInsert, this.selectors.slice(i + 1)); + selCnt += selectorsToInsert.length; + removeSelector = true; + } + } + } + if (removeSelector) { + selCnt -= 1; + this.selectors.splice(i, 1); + i -= 1; + continue; + } + else { + selectors[i] = selector; + if (selector.evaldCondition) { + hasOnePassingSelector = true; + } } } defaultFunc.reset(); @@ -162,7 +214,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++) { @@ -507,7 +559,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)); } @@ -516,7 +574,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; } @@ -550,7 +608,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)); } @@ -684,7 +748,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 fb866a9b3..6d2d32c15 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 c90d6e69f..1ae0f4f7d 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 5c4128823..f77008745 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..68ff49948 --- /dev/null +++ b/test/css/parse-interpolation.css @@ -0,0 +1,21 @@ +input[type=text]:focus, +input[type=email]:focus, +input[type=password]:focus, +textarea:focus { + foo: bar; +} +.a + .z, +.b + .z, +.c + .z { + color: blue; +} +.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/less-test.js b/test/less-test.js index 9ab051d7f..34f4c2143 100644 --- a/test/less-test.js +++ b/test/less-test.js @@ -14,7 +14,7 @@ module.exports = function() { var oneTestOnly = process.argv[2], isFinished = false; - var isVerbose = process.env.npm_config_loglevel === 'verbose'; + var isVerbose = process.env.npm_config_loglevel !== 'concise'; var normalFolder = 'test/less'; var bomFolder = 'test/less-bom'; diff --git a/test/less/parse-interpolation.less b/test/less/parse-interpolation.less new file mode 100644 index 000000000..a080c7474 --- /dev/null +++ b/test/less/parse-interpolation.less @@ -0,0 +1,29 @@ +@inputs: input[type=text], input[type=email], input[type=password], textarea; + +@{inputs} { + &:focus { + foo: bar; + } +} + +@classes: ~".a, .b, .c"; + +@{classes} { + + .z { + color: blue; + } +} + +@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