Skip to content

Commit b8140d4

Browse files
authored
Fixes #1421 - re-parses variable-interpolated elements to selectors (no.2) (#3227)
* Fix element to selector list conversion, passing all tests! * Add passing test from #3098 * Added passing test example from #1817 * Allow lists to be re-evaluated as selectors (Fixes #1694)
1 parent 7a12d2f commit b8140d4

10 files changed

+192
-38
lines changed

lib/less/parser/parser.js

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ var Parser = function Parser(context, imports, fileInfo) {
814814
if (!e) {
815815
break;
816816
}
817-
elem = new(tree.Element)(c, e, elemIndex, fileInfo);
817+
elem = new(tree.Element)(c, e, false, elemIndex, fileInfo);
818818
if (elements) {
819819
elements.push(elem);
820820
} else {
@@ -1101,7 +1101,7 @@ var Parser = function Parser(context, imports, fileInfo) {
11011101
}
11021102
}
11031103

1104-
if (e) { return new(tree.Element)(c, e, index, fileInfo); }
1104+
if (e) { return new(tree.Element)(c, e, e instanceof tree.Variable, index, fileInfo); }
11051105
},
11061106

11071107
//
@@ -1181,6 +1181,30 @@ var Parser = function Parser(context, imports, fileInfo) {
11811181
if (elements) { return new(tree.Selector)(elements, allExtends, condition, index, fileInfo); }
11821182
if (allExtends) { error('Extend must be used to extend a selector, it cannot be used on its own'); }
11831183
},
1184+
selectors: function () {
1185+
var s, selectors;
1186+
while (true) {
1187+
s = this.selector();
1188+
if (!s) {
1189+
break;
1190+
}
1191+
if (selectors) {
1192+
selectors.push(s);
1193+
} else {
1194+
selectors = [ s ];
1195+
}
1196+
parserInput.commentStore.length = 0;
1197+
if (s.condition && selectors.length > 1) {
1198+
error("Guards are only currently allowed on a single selector.");
1199+
}
1200+
if (!parserInput.$char(',')) { break; }
1201+
if (s.condition) {
1202+
error("Guards are only currently allowed on a single selector.");
1203+
}
1204+
parserInput.commentStore.length = 0;
1205+
}
1206+
return selectors;
1207+
},
11841208
attribute: function () {
11851209
if (!parserInput.$char('[')) { return; }
11861210

@@ -1232,34 +1256,15 @@ var Parser = function Parser(context, imports, fileInfo) {
12321256
// div, .class, body > p {...}
12331257
//
12341258
ruleset: function () {
1235-
var selectors, s, rules, debugInfo;
1259+
var selectors, rules, debugInfo;
12361260

12371261
parserInput.save();
12381262

12391263
if (context.dumpLineNumbers) {
12401264
debugInfo = getDebugInfo(parserInput.i);
12411265
}
12421266

1243-
while (true) {
1244-
s = this.selector();
1245-
if (!s) {
1246-
break;
1247-
}
1248-
if (selectors) {
1249-
selectors.push(s);
1250-
} else {
1251-
selectors = [ s ];
1252-
}
1253-
parserInput.commentStore.length = 0;
1254-
if (s.condition && selectors.length > 1) {
1255-
error('Guards are only currently allowed on a single selector.');
1256-
}
1257-
if (!parserInput.$char(',')) { break; }
1258-
if (s.condition) {
1259-
error('Guards are only currently allowed on a single selector.');
1260-
}
1261-
parserInput.commentStore.length = 0;
1262-
}
1267+
selectors = this.selectors();
12631268

12641269
if (selectors && (rules = this.block())) {
12651270
parserInput.forget();

lib/less/tree/element.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var Node = require('./node'),
22
Paren = require('./paren'),
33
Combinator = require('./combinator');
44

5-
var Element = function (combinator, value, index, currentFileInfo, visibilityInfo) {
5+
var Element = function (combinator, value, isVariable, index, currentFileInfo, visibilityInfo) {
66
this.combinator = combinator instanceof Combinator ?
77
combinator : new Combinator(combinator);
88

@@ -13,6 +13,7 @@ var Element = function (combinator, value, index, currentFileInfo, visibilityInf
1313
} else {
1414
this.value = '';
1515
}
16+
this.isVariable = isVariable;
1617
this._index = index;
1718
this._fileInfo = currentFileInfo;
1819
this.copyVisibilityInfo(visibilityInfo);
@@ -30,12 +31,14 @@ Element.prototype.accept = function (visitor) {
3031
Element.prototype.eval = function (context) {
3132
return new Element(this.combinator,
3233
this.value.eval ? this.value.eval(context) : this.value,
34+
this.isVariable,
3335
this.getIndex(),
3436
this.fileInfo(), this.visibilityInfo());
3537
};
3638
Element.prototype.clone = function () {
3739
return new Element(this.combinator,
3840
this.value,
41+
this.isVariable,
3942
this.getIndex(),
4043
this.fileInfo(), this.visibilityInfo());
4144
};

lib/less/tree/mixin-definition.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ var Selector = require('./selector'),
88

99
var Definition = function (name, params, rules, condition, variadic, frames, visibilityInfo) {
1010
this.name = name;
11-
this.selectors = [new Selector([new Element(null, name, this._index, this._fileInfo)])];
11+
this.selectors = [new Selector([new Element(null, name, false, this._index, this._fileInfo)])];
1212
this.params = params;
1313
this.condition = condition;
1414
this.variadic = variadic;

lib/less/tree/ruleset.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,47 @@ Ruleset.prototype.accept = function (visitor) {
4141
}
4242
};
4343
Ruleset.prototype.eval = function (context) {
44-
var thisSelectors = this.selectors, selectors,
45-
selCnt, selector, i, hasOnePassingSelector = false;
44+
var that = this, selectors, selCnt, selector, i, hasVariable, hasOnePassingSelector = false;
4645

47-
if (thisSelectors && (selCnt = thisSelectors.length)) {
46+
if (this.selectors && (selCnt = this.selectors.length)) {
4847
selectors = new Array(selCnt);
4948
defaultFunc.error({
5049
type: 'Syntax',
5150
message: 'it is currently only allowed in parametric mixin guards,'
5251
});
52+
5353
for (i = 0; i < selCnt; i++) {
54-
selector = thisSelectors[i].eval(context);
54+
selector = this.selectors[i].eval(context);
55+
for (var j = 0; j < selector.elements.length; j++) {
56+
if (selector.elements[j].isVariable) {
57+
hasVariable = true;
58+
break;
59+
}
60+
}
5561
selectors[i] = selector;
5662
if (selector.evaldCondition) {
5763
hasOnePassingSelector = true;
5864
}
5965
}
66+
67+
if (hasVariable) {
68+
var toParseSelectors = new Array(selCnt);
69+
for (i = 0; i < selCnt; i++) {
70+
selector = selectors[i];
71+
toParseSelectors[i] = selector.toCSS(context);
72+
}
73+
this.parse.parseNode(
74+
toParseSelectors.join(','),
75+
["selectors"],
76+
selectors[0].getIndex(),
77+
selectors[0].fileInfo(),
78+
function(err, result) {
79+
if (result) {
80+
selectors = utils.flattenArray(result);
81+
}
82+
});
83+
}
84+
6085
defaultFunc.reset();
6186
} else {
6287
hasOnePassingSelector = true;
@@ -162,7 +187,7 @@ Ruleset.prototype.eval = function (context) {
162187
// for rulesets, check if it is a css guard and can be removed
163188
if (rule instanceof Ruleset && rule.selectors && rule.selectors.length === 1) {
164189
// check if it can be folded in (e.g. & where)
165-
if (rule.selectors[0].isJustParentSelector()) {
190+
if (rule.selectors[0] && rule.selectors[0].isJustParentSelector()) {
166191
rsRules.splice(i--, 1);
167192

168193
for (var j = 0; (subRule = rule.rules[j]); j++) {
@@ -511,7 +536,13 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) {
511536
} else {
512537
var insideParent = new Array(elementsToPak.length);
513538
for (j = 0; j < elementsToPak.length; j++) {
514-
insideParent[j] = new Element(null, elementsToPak[j], originalElement._index, originalElement._fileInfo);
539+
insideParent[j] = new Element(
540+
null,
541+
elementsToPak[j],
542+
originalElement.isVariable,
543+
originalElement._index,
544+
originalElement._fileInfo
545+
);
515546
}
516547
replacementParen = new Paren(new Selector(insideParent));
517548
}
@@ -520,7 +551,7 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) {
520551

521552
function createSelector(containedElement, originalElement) {
522553
var element, selector;
523-
element = new Element(null, containedElement, originalElement._index, originalElement._fileInfo);
554+
element = new Element(null, containedElement, originalElement.isVariable, originalElement._index, originalElement._fileInfo);
524555
selector = new Selector([element]);
525556
return selector;
526557
}
@@ -545,7 +576,8 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) {
545576
}
546577

547578
if (addPath.length > 0) {
548-
// /deep/ is a combinator that is valid without anything in front of it
579+
// /deep/ is a CSS4 selector - (removed, so should deprecate)
580+
// that is valid without anything in front of it
549581
// so if the & does not have a combinator that is "" or " " then
550582
// and there is a combinator on the parent, then grab that.
551583
// 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) {
554586
combinator = parentEl.combinator;
555587
}
556588
// join the elements so far with the first part of the parent
557-
newJoinedSelector.elements.push(new Element(combinator, parentEl.value, replacedElement._index, replacedElement._fileInfo));
589+
newJoinedSelector.elements.push(new Element(
590+
combinator,
591+
parentEl.value,
592+
replacedElement.isVariable,
593+
replacedElement._index,
594+
replacedElement._fileInfo
595+
));
558596
newJoinedSelector.elements = newJoinedSelector.elements.concat(addPath[0].elements.slice(1));
559597
}
560598

@@ -688,7 +726,7 @@ Ruleset.prototype.joinSelector = function (paths, context, selector) {
688726
// the combinator used on el should now be applied to the next element instead so that
689727
// it is not lost
690728
if (sel.length > 0) {
691-
sel[0].elements.push(new Element(el.combinator, '', el._index, el._fileInfo));
729+
sel[0].elements.push(new Element(el.combinator, '', el.isVariable, el._index, el._fileInfo));
692730
}
693731
selectorsMultiplied.push(sel);
694732
}

lib/less/tree/selector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Selector.prototype.getElements = function(els) {
5454
return els;
5555
};
5656
Selector.prototype.createEmptySelectors = function() {
57-
var el = new Element('', '&', this._index, this._fileInfo),
57+
var el = new Element('', '&', false, this._index, this._fileInfo),
5858
sels = [new Selector([el], null, null, this._index, this._fileInfo)];
5959
sels[0].mediaEmpty = true;
6060
return sels;

lib/less/utils.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* jshint proto: true */
2-
module.exports = {
2+
var utils = {
33
getLocation: function(index, inputStream) {
44
var n = index + 1,
55
line = null,
@@ -65,5 +65,21 @@ module.exports = {
6565
}
6666
}
6767
return obj1;
68+
},
69+
flattenArray: function(arr, result) {
70+
result = result || [];
71+
for (var i = 0, length = arr.length; i < length; i++) {
72+
var value = arr[i];
73+
if (Array.isArray(value)) {
74+
utils.flattenArray(value, result);
75+
} else {
76+
if (value !== undefined) {
77+
result.push(value);
78+
}
79+
}
80+
}
81+
return result;
6882
}
6983
};
84+
85+
module.exports = utils;

lib/less/visitors/extend-visitor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ ProcessExtendsVisitor.prototype = {
385385
firstElement = new tree.Element(
386386
match.initialCombinator,
387387
replacementSelector.elements[0].value,
388+
replacementSelector.elements[0].isVariable,
388389
replacementSelector.elements[0].getIndex(),
389390
replacementSelector.elements[0].fileInfo()
390391
);

test/css/parse-interpolation.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
input[type=text]:focus,
2+
input[type=email]:focus,
3+
input[type=password]:focus,
4+
textarea:focus {
5+
foo: bar;
6+
}
7+
.a + .z,
8+
.b + .z,
9+
.c + .z {
10+
color: blue;
11+
}
12+
.bar .d.a,
13+
.bar .b,
14+
.c.bar:hover,
15+
.bar baz {
16+
color: blue;
17+
}
18+
.a + .e,
19+
.b.c + .e,
20+
.d + .e {
21+
foo: bar;
22+
}
23+
input[class="text"],
24+
input.text {
25+
background: red;
26+
}
27+
.master-page-1 .selector-1,
28+
.master-page-1 .selector-2 {
29+
background-color: red;
30+
}
31+
.fruit-apple,
32+
.fruit-satsuma,
33+
.fruit-banana,
34+
.fruit-pear {
35+
content: "Just a test.";
36+
}

test/css/permissive-parse.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
@-moz-whatever (foo: "(" bam ")") {
2222
bar: foo;
2323
}
24-
#selector, .bar, foo[attr="blah"] {
24+
#selector,
25+
.bar,
26+
foo[attr="blah"] {
2527
bar: value;
2628
}
2729
@media (min-width: 640px) {

test/less/parse-interpolation.less

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@inputs: input[type=text], input[type=email], input[type=password], textarea;
2+
3+
@{inputs} {
4+
&:focus {
5+
foo: bar;
6+
}
7+
}
8+
9+
@classes: .a, .b, .c;
10+
11+
@{classes} {
12+
+ .z {
13+
color: blue;
14+
}
15+
}
16+
17+
.bar {
18+
.d@{classes}&:hover, baz {
19+
color: blue;
20+
}
21+
}
22+
23+
@c: ~'.a, .b';
24+
@d: ~'.c, .d';
25+
@e: ~' + .e';
26+
27+
@{c}@{d} {
28+
@{e} {
29+
foo: bar;
30+
}
31+
}
32+
33+
@textClasses: ~'&[class="text"], &.text';
34+
35+
input {
36+
@{textClasses} {
37+
background: red;
38+
}
39+
}
40+
41+
@my-selector: ~'.selector-1, .selector-2';
42+
.master-page-1 {
43+
@{my-selector} {
44+
background-color: red;
45+
}
46+
}
47+
48+
@list: apple, satsuma, banana, pear;
49+
@{list} {
50+
.fruit-& {
51+
content: "Just a test.";
52+
}
53+
}

0 commit comments

Comments
 (0)