From c383326da6f5e8d5b0b606f74540e74933383a84 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sat, 2 Jun 2018 14:21:10 -0700 Subject: [PATCH 1/5] WIP - partially solves 1241 but breaks other tests --- lib/less/parser/parser.js | 51 +++++++++++++------------ lib/less/tree/element.js | 5 ++- lib/less/tree/mixin-definition.js | 2 +- lib/less/tree/ruleset.js | 58 ++++++++++++++++++++++++----- lib/less/tree/selector.js | 2 +- lib/less/utils.js | 18 ++++++++- lib/less/visitors/extend-visitor.js | 1 + test/css/parse-interpolation.css | 6 +++ test/less-test.js | 2 +- test/less/parse-interpolation.less | 7 ++++ 10 files changed, 115 insertions(+), 37 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..4be843d22 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -41,17 +41,45 @@ 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); + selector = this.selectors[i].eval(context); + var removeSelector = false; + for (var j = 0; j < selector.elements.length; j++) { + var el = selector.elements[j]; + if (el.isVariable && 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); + that.selectors = that.selectors.slice(0, i + 1) + .concat(result, that.selectors.slice(i + 1)); + selCnt += result.length; + removeSelector = true; + } + }); + } + // if (el.isVariable && selector._fileInfo.filename.indexOf('parse-') > -1) { + // console.log(el); + // } + } + if (removeSelector) { + selCnt -= 1; + this.selectors.splice(i, 1); + continue; + } selectors[i] = selector; if (selector.evaldCondition) { hasOnePassingSelector = true; @@ -162,7 +190,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 +535,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 +550,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 +584,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 +724,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..d0e37190f --- /dev/null +++ b/test/css/parse-interpolation.css @@ -0,0 +1,6 @@ +input[type=text]:focus, +input[type=email]:focus, +input[type=password]:focus, +textarea:focus { + foo: bar; +} \ No newline at end of file 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..bdcb65176 --- /dev/null +++ b/test/less/parse-interpolation.less @@ -0,0 +1,7 @@ +@inputs: input[type=text], input[type=email], input[type=password], textarea; + +@{inputs} { + &:focus { + foo: bar; + } +} \ No newline at end of file From 15d662a388a389d84b1a6206dd40d77a78dad7ea Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sun, 3 Jun 2018 13:23:09 -0700 Subject: [PATCH 2/5] Fix element to selector list conversion, passing all tests! --- lib/less/tree/ruleset.js | 24 ++++++++++++++---------- test/css/parse-interpolation.css | 8 ++++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index 4be843d22..e672ea0e7 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -54,6 +54,7 @@ Ruleset.prototype.eval = function (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 && typeof el.value.value === 'string') { this.parse.parseNode( el.value.value, @@ -64,25 +65,28 @@ Ruleset.prototype.eval = function (context) { el.isVariable = false; if (result) { result = utils.flattenArray(result); - that.selectors = that.selectors.slice(0, i + 1) - .concat(result, that.selectors.slice(i + 1)); - selCnt += result.length; - removeSelector = true; + // If the parsed element matches itself, it's still an element + if (result.length !== 1 || el.value.value !== result[0].elements[0].value) { + that.selectors = that.selectors.slice(0, i + 1) + .concat(result, that.selectors.slice(i + 1)); + selCnt += result.length; + removeSelector = true; + } } }); } - // if (el.isVariable && selector._fileInfo.filename.indexOf('parse-') > -1) { - // console.log(el); - // } } if (removeSelector) { selCnt -= 1; this.selectors.splice(i, 1); + i -= 1; continue; } - selectors[i] = selector; - if (selector.evaldCondition) { - hasOnePassingSelector = true; + else { + selectors[i] = selector; + if (selector.evaldCondition) { + hasOnePassingSelector = true; + } } } defaultFunc.reset(); diff --git a/test/css/parse-interpolation.css b/test/css/parse-interpolation.css index d0e37190f..6c3276107 100644 --- a/test/css/parse-interpolation.css +++ b/test/css/parse-interpolation.css @@ -1,6 +1,6 @@ -input[type=text]:focus, -input[type=email]:focus, -input[type=password]:focus, +input[type=text]:focus, +input[type=email]:focus, +input[type=password]:focus, textarea:focus { foo: bar; -} \ No newline at end of file +} From d1ed1fe9fb045de67776196ae1b79a0588c60ab8 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sun, 3 Jun 2018 13:28:21 -0700 Subject: [PATCH 3/5] Add passing test from #3098 --- test/css/parse-interpolation.css | 5 +++++ test/less/parse-interpolation.less | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/test/css/parse-interpolation.css b/test/css/parse-interpolation.css index 6c3276107..fc82b3dca 100644 --- a/test/css/parse-interpolation.css +++ b/test/css/parse-interpolation.css @@ -4,3 +4,8 @@ input[type=password]:focus, textarea:focus { foo: bar; } +.a + .z, +.b + .z, +.c + .z { + color: blue; +} diff --git a/test/less/parse-interpolation.less b/test/less/parse-interpolation.less index bdcb65176..0d82eac09 100644 --- a/test/less/parse-interpolation.less +++ b/test/less/parse-interpolation.less @@ -4,4 +4,12 @@ &:focus { foo: bar; } -} \ No newline at end of file +} + +@classes: ~".a, .b, .c"; + +@{classes} { + + .z { + color: blue; + } +} \ No newline at end of file From 94e7c505dd6851a95cf85b5f8178dd01ac346535 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sun, 3 Jun 2018 13:33:02 -0700 Subject: [PATCH 4/5] Added passing test example from #1817 --- test/css/parse-interpolation.css | 4 ++++ test/less/parse-interpolation.less | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/test/css/parse-interpolation.css b/test/css/parse-interpolation.css index fc82b3dca..9a040896e 100644 --- a/test/css/parse-interpolation.css +++ b/test/css/parse-interpolation.css @@ -9,3 +9,7 @@ textarea:focus { .c + .z { color: blue; } +.master-page-1 .selector-1, +.master-page-1 .selector-2 { + background-color: red; +} diff --git a/test/less/parse-interpolation.less b/test/less/parse-interpolation.less index 0d82eac09..a16aa2957 100644 --- a/test/less/parse-interpolation.less +++ b/test/less/parse-interpolation.less @@ -12,4 +12,11 @@ + .z { color: blue; } +} + +@my-selector: ~'.selector-1, .selector-2'; +.master-page-1 { + @{my-selector} { + background-color: red; + } } \ No newline at end of file From 9cec20e13c60e49eda10746e17f5a3f2a9de5866 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sun, 3 Jun 2018 14:07:55 -0700 Subject: [PATCH 5/5] Allow lists to be re-evaluated as selectors (Fixes #1694) --- lib/less/tree/ruleset.js | 56 ++++++++++++++++++++---------- test/css/parse-interpolation.css | 6 ++++ test/less/parse-interpolation.less | 7 ++++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index e672ea0e7..c57facc11 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -55,25 +55,45 @@ Ruleset.prototype.eval = function (context) { 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 && 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) { - that.selectors = that.selectors.slice(0, i + 1) - .concat(result, that.selectors.slice(i + 1)); - selCnt += result.length; - removeSelector = true; + 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) { diff --git a/test/css/parse-interpolation.css b/test/css/parse-interpolation.css index 9a040896e..68ff49948 100644 --- a/test/css/parse-interpolation.css +++ b/test/css/parse-interpolation.css @@ -13,3 +13,9 @@ textarea:focus { .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/parse-interpolation.less b/test/less/parse-interpolation.less index a16aa2957..a080c7474 100644 --- a/test/less/parse-interpolation.less +++ b/test/less/parse-interpolation.less @@ -19,4 +19,11 @@ @{my-selector} { background-color: red; } +} + +@list: apple, satsuma, banana, pear; +@{list} { + .fruit-& { + content: "Just a test."; + } } \ No newline at end of file