less 4.6.7 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,10 +17,25 @@ const deprecations = {
17
17
  description: 'The ./ operator is deprecated.'
18
18
  },
19
19
  'variable-in-unknown-value': {
20
- description: '@[ident] in custom property values is treated as literal text.'
20
+ description: '@variable in custom property values is treated as literal text.'
21
+ },
22
+ 'variable-in-at-rule-prelude': {
23
+ description: 'A bare @variable in an at-rule prelude (e.g. @media @foo) is deprecated. Use @{variable} interpolation instead.'
24
+ },
25
+ 'numeric-variable-name': {
26
+ description: 'Variable names beginning with a number are deprecated and will be removed in Less 5.x.'
27
+ },
28
+ 'dash-only-variable-name': {
29
+ description: 'The dash-only variable names @- and @{-} are deprecated and will be removed in Less 5.x.'
30
+ },
31
+ 'dash-only-mixin-name': {
32
+ description: 'The dash-only mixin names .-() and #-() are deprecated and will be removed in Less 5.x.'
33
+ },
34
+ 'dynamic-charset': {
35
+ description: 'Dynamic @charset interpolation is deprecated and will be removed in Less 5.x.'
21
36
  },
22
37
  'property-in-unknown-value': {
23
- description: '$[ident] in custom property values is treated as literal text.'
38
+ description: '$property in custom property values is treated as literal text.'
24
39
  },
25
40
  'js-eval': {
26
41
  description: 'Inline JavaScript evaluation (backtick expressions) is deprecated and will be removed in Less 5.x.'
@@ -204,12 +204,23 @@ export default () => {
204
204
  /**
205
205
  * Permissive parsing. Ignores everything except matching {} [] () and quotes
206
206
  * until matching token (outside of blocks)
207
+ *
208
+ * @param {string|RegExp} tok - stop token
209
+ * @param {boolean} [detectBareVar] - when set, also record the position of the
210
+ * first bare `@variable` reference (not `@{interpolation}`) that appears at
211
+ * PAREN depth 0 — i.e. a structural reference, not a declaration value inside
212
+ * `(...)`. Reuses this single pass (which already skips strings/comments) so
213
+ * callers don't re-scan the text. Exposed as `.bareVarIndex` on the returned
214
+ * group array (or null). `[...]`/`{...}` do NOT shield a reference — only
215
+ * `(...)` (a declaration-value group) does.
207
216
  */
208
- parserInput.$parseUntil = tok => {
217
+ parserInput.$parseUntil = (tok, detectBareVar) => {
209
218
  let quote = '';
210
219
  let returnVal = null;
211
220
  let inComment = false;
212
221
  let blockDepth = 0;
222
+ let parenDepth = 0;
223
+ let bareVarIndex = null;
213
224
  const blockStack = [];
214
225
  const parseGroups = [];
215
226
  const length = input.length;
@@ -249,6 +260,12 @@ export default () => {
249
260
  i++;
250
261
  continue;
251
262
  }
263
+ if (detectBareVar && bareVarIndex === null && nextChar === '@' && parenDepth === 0) {
264
+ // A bare `@ident` (not `@{interpolation}`) outside any `(...)` —
265
+ // a structural reference. Strings/comments are already skipped above.
266
+ const after = input.charAt(i + 1);
267
+ if (after && /[-\w]/.test(after)) { bareVarIndex = i; }
268
+ }
252
269
  switch (nextChar) {
253
270
  case '\\':
254
271
  i++;
@@ -284,6 +301,7 @@ export default () => {
284
301
  case '(':
285
302
  blockStack.push(')');
286
303
  blockDepth++;
304
+ parenDepth++;
287
305
  break;
288
306
  case '[':
289
307
  blockStack.push(']');
@@ -295,6 +313,7 @@ export default () => {
295
313
  const expected = blockStack.pop();
296
314
  if (nextChar === expected) {
297
315
  blockDepth--;
316
+ if (nextChar === ')' && parenDepth > 0) { parenDepth--; }
298
317
  } else {
299
318
  // move the parser to the error and return expected
300
319
  skipWhitespace(i - startPos);
@@ -310,6 +329,7 @@ export default () => {
310
329
  }
311
330
  } while (loop);
312
331
 
332
+ if (Array.isArray(returnVal)) { returnVal.bareVarIndex = bareVarIndex; }
313
333
  return returnVal ? returnVal : null;
314
334
  }
315
335
 
@@ -62,6 +62,10 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
62
62
 
63
63
  const deprecationHandler = new DeprecationHandler();
64
64
 
65
+ // Tracks `${deprecationId}@${index}` pairs already warned about, so a source
66
+ // position that gets re-parsed via parser backtracking only warns once.
67
+ const warnedDeprecations = new Set();
68
+
65
69
  /**
66
70
  * @param {string} msg
67
71
  * @param {number} index
@@ -71,6 +75,11 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
71
75
  function warn(msg, index, type, deprecationId) {
72
76
  if (context.quiet) { return; }
73
77
  if (deprecationId && context.quietDeprecations) { return; }
78
+ if (deprecationId) {
79
+ const key = `${deprecationId}@${index ?? parserInput.i}`;
80
+ if (warnedDeprecations.has(key)) { return; }
81
+ warnedDeprecations.add(key);
82
+ }
74
83
  if (deprecationId && !deprecationHandler.shouldWarn(deprecationId)) { return; }
75
84
 
76
85
  logger.warn(
@@ -86,6 +95,75 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
86
95
  );
87
96
  }
88
97
 
98
+ /**
99
+ * Warn that a bare `@variable` reference is being used in a non-value
100
+ * position (an at-rule prelude, name, or identifier), where it still
101
+ * resolves today but is deprecated in favour of `@{variable}` interpolation.
102
+ *
103
+ * @param {number} index - source position of the bare reference
104
+ */
105
+ function warnBareAtRuleVariable(index) {
106
+ warn('A bare @variable in an at-rule prelude is deprecated. Use @{variable} interpolation instead.', index, 'DEPRECATED', 'variable-in-at-rule-prelude');
107
+ }
108
+
109
+ /**
110
+ * Numeric-leading variable names are a Less extension rather than valid CSS
111
+ * identifier syntax. Keep accepting them through Less 4, but make the Less 5
112
+ * migration visible at every actual variable reference or definition.
113
+ *
114
+ * @param {string} name - either an @-prefixed name or the name inside @{...}
115
+ * @param {number} index - source position of the variable token
116
+ */
117
+ function warnNumericVariableName(name, index) {
118
+ if (/^(?:@@?)?[0-9]/.test(name)) {
119
+ warn('Variable names beginning with a number are deprecated and will be removed in Less 5.x. Rename the variable to start with a valid identifier character.', index, 'DEPRECATED', 'numeric-variable-name');
120
+ }
121
+ }
122
+
123
+ /**
124
+ * A lone '-' is not a CSS identifier. Less historically accepts it as a
125
+ * variable name; retain that in Less 4 only and warn on definitions,
126
+ * ordinary references, and interpolation references.
127
+ *
128
+ * @param {string} name - either @-/@@-, or the name inside @{...}
129
+ * @param {number} index - source position of the variable token
130
+ */
131
+ function warnDashOnlyVariableName(name, index) {
132
+ const bareName = name.replace(/^@@?/, '');
133
+ if (bareName === '-') {
134
+ warn('The dash-only variable names @- and @{-} are deprecated and will be removed in Less 5.x. Rename the variable to use a valid identifier.', index, 'DEPRECATED', 'dash-only-variable-name');
135
+ }
136
+ }
137
+
138
+ /**
139
+ * A lone '-' is not a CSS identifier. Less historically accepts it as a mixin
140
+ * name after '.' or '#'; retain that in Less 4 only.
141
+ *
142
+ * @param {string} name
143
+ * @param {number} index
144
+ */
145
+ function warnDashOnlyMixinName(name, index) {
146
+ if (name === '.-' || name === '#-') {
147
+ warn('The dash-only mixin names .-() and #-() are deprecated and will be removed in Less 5.x. Rename the mixin to use a valid CSS identifier.', index, 'DEPRECATED', 'dash-only-mixin-name');
148
+ }
149
+ }
150
+
151
+ /**
152
+ * CSS @charset is a source-header declaration, not a general dynamic at-rule.
153
+ * Less 4 keeps its historical interpolation behavior for compatibility, but
154
+ * makes each dynamic spelling visible before Less 5 rejects it.
155
+ *
156
+ * @param {import('../tree/node.js').default} value
157
+ * @param {number} index - source position of the @charset token
158
+ */
159
+ function warnDynamicCharset(value, index) {
160
+ if (value instanceof tree.Variable ||
161
+ (value instanceof tree.Quoted &&
162
+ (value.containsVariables() || value.value.match(value.propRegex)))) {
163
+ warn('Dynamic @charset interpolation is deprecated and will be removed in Less 5.x. Use a static quoted encoding declaration instead.', index, 'DEPRECATED', 'dynamic-charset');
164
+ }
165
+ }
166
+
89
167
  function expect(arg, msg) {
90
168
  // some older browsers return typeof 'function' for RegExp
91
169
  const result = (arg instanceof Function) ? arg.call(parsers) : parserInput.$re(arg);
@@ -539,7 +617,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
539
617
  }
540
618
 
541
619
  function condition() {
542
- return [expect(parsers.condition, 'expected condition')];
620
+ return [expect(() => parsers.condition(false, true), 'expected condition')];
543
621
  }
544
622
  },
545
623
 
@@ -664,6 +742,8 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
664
742
 
665
743
  parserInput.save();
666
744
  if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^@@?[\w-]+/))) {
745
+ warnNumericVariableName(name, index);
746
+ warnDashOnlyVariableName(name, index);
667
747
  ch = parserInput.currentChar();
668
748
  if ((ch === '(' && !parserInput.prevChar().match(/^\s/))
669
749
  || (ch === '[' && !parserInput.prevChar().match(/^\s/))) {
@@ -686,6 +766,8 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
686
766
  const index = parserInput.i;
687
767
 
688
768
  if (parserInput.currentChar() === '@' && (curly = parserInput.$re(/^@\{([\w-]+)\}/))) {
769
+ warnNumericVariableName(curly[1], index);
770
+ warnDashOnlyVariableName(curly[1], index);
689
771
  return new(tree.Variable)(`@${curly[1]}`, index + currentIndex, fileInfo);
690
772
  }
691
773
  },
@@ -808,7 +890,11 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
808
890
  variable: function () {
809
891
  let name;
810
892
 
811
- if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^(@[\w-]+)\s*:/))) { return name[1]; }
893
+ if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^(@[\w-]+)\s*:/))) {
894
+ warnNumericVariableName(name[1], parserInput.i - name[0].length);
895
+ warnDashOnlyVariableName(name[1], parserInput.i - name[0].length);
896
+ return name[1];
897
+ }
812
898
  },
813
899
 
814
900
  //
@@ -838,6 +924,8 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
838
924
  }
839
925
 
840
926
  if (!inValue) {
927
+ warnNumericVariableName(name[1], i);
928
+ warnDashOnlyVariableName(name[1], i);
841
929
  name = name[1];
842
930
  }
843
931
 
@@ -994,6 +1082,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
994
1082
  if (inValue || parsers.end()) {
995
1083
  parserInput.forget();
996
1084
  const mixin = new(tree.mixin.Call)(elements, args, index + currentIndex, fileInfo, !lookups && important);
1085
+ for (const element of elements) {
1086
+ warnDashOnlyMixinName(element.value, element._index - currentIndex);
1087
+ }
997
1088
  if (lookups) {
998
1089
  return new tree.NamespaceValue(mixin, lookups);
999
1090
  }
@@ -1190,6 +1281,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1190
1281
  let ruleset;
1191
1282
  let cond;
1192
1283
  let variadic = false;
1284
+ const index = parserInput.i;
1193
1285
  if ((parserInput.currentChar() !== '.' && parserInput.currentChar() !== '#') ||
1194
1286
  parserInput.peek(/^[^{]*\}/)) {
1195
1287
  return;
@@ -1225,6 +1317,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1225
1317
 
1226
1318
  if (ruleset) {
1227
1319
  parserInput.forget();
1320
+ warnDashOnlyMixinName(name, index);
1228
1321
  return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);
1229
1322
  } else {
1230
1323
  parserInput.restore();
@@ -1636,7 +1729,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1636
1729
  if (parserInput.$char(';')) {
1637
1730
  value = new Anonymous('');
1638
1731
  } else {
1639
- value = this.permissiveValue(/[;}]/, true);
1732
+ value = this.permissiveValue(/[;}]/);
1640
1733
  }
1641
1734
  }
1642
1735
  // Try to store values as anonymous
@@ -1695,8 +1788,12 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1695
1788
  * math is allowed.
1696
1789
  *
1697
1790
  * @param {RexExp} untilTokens - Characters to stop parsing at
1791
+ * @param {boolean} [deprecateVariables] - when set, this is an at-rule
1792
+ * prelude (non-value position); accept `@{var}` interpolation and warn
1793
+ * on a bare `@var` reference (which resolves today but is deprecated).
1698
1794
  */
1699
- permissiveValue: function (untilTokens) {
1795
+ permissiveValue: function (untilTokens, deprecateVariables) {
1796
+ const entities = this.entities;
1700
1797
  let i;
1701
1798
  let e;
1702
1799
  let done;
@@ -1723,7 +1820,20 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1723
1820
  value.push(e);
1724
1821
  continue;
1725
1822
  }
1726
- e = this.entity();
1823
+ if (deprecateVariables) {
1824
+ // In an at-rule prelude, `@{var}` interpolation is the supported
1825
+ // form; consume it here so its `{` is not mistaken for a block.
1826
+ e = entities.variableCurly();
1827
+ if (!e) {
1828
+ const varIndex = parserInput.i;
1829
+ e = this.entity();
1830
+ if (e && e.type === 'Variable') {
1831
+ warnBareAtRuleVariable(varIndex);
1832
+ }
1833
+ }
1834
+ } else {
1835
+ e = this.entity();
1836
+ }
1727
1837
  if (e) {
1728
1838
  value.push(e);
1729
1839
  }
@@ -1750,7 +1860,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1750
1860
  }
1751
1861
  parserInput.save();
1752
1862
 
1753
- value = parserInput.$parseUntil(tok);
1863
+ value = parserInput.$parseUntil(tok, deprecateVariables);
1754
1864
 
1755
1865
  if (value) {
1756
1866
  if (typeof value === 'string') {
@@ -1760,6 +1870,14 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1760
1870
  parserInput.forget();
1761
1871
  return new tree.Anonymous('', index);
1762
1872
  }
1873
+ // At-rule prelude: `$parseUntil` (deprecateVariables) records the
1874
+ // first bare `@var` it saw outside any `(...)` in its single pass —
1875
+ // a structural reference (`[...]`/`{...}` don't shield it, only a
1876
+ // declaration-value `(...)` does). Warn once here rather than
1877
+ // re-scanning the text.
1878
+ if (deprecateVariables && value.bareVarIndex !== null && value.bareVarIndex !== undefined) {
1879
+ warnBareAtRuleVariable(value.bareVarIndex);
1880
+ }
1763
1881
  /** @type {string} */
1764
1882
  let item;
1765
1883
  for (i = 0; i < value.length; i++) {
@@ -1776,11 +1894,14 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1776
1894
  const quote = new tree.Quoted('\'', item, true, index, fileInfo);
1777
1895
  const variableRegex = /@([\w-]+)/g;
1778
1896
  const propRegex = /\$([\w-]+)/g;
1779
- if (variableRegex.test(item)) {
1780
- warn('@[ident] in unknown values will not be evaluated as variables in the future. Use @{[ident]}', index, 'DEPRECATED', 'variable-in-unknown-value');
1897
+ // At-rule preludes are handled once above via
1898
+ // `value.bareVarIndex`; the `variable-in-unknown-value`
1899
+ // notice is for unknown declaration values only.
1900
+ if (!deprecateVariables && variableRegex.test(item)) {
1901
+ warn('@variable in unknown values will not be evaluated as variables in the future. Use @{variable}', index, 'DEPRECATED', 'variable-in-unknown-value');
1781
1902
  }
1782
1903
  if (propRegex.test(item)) {
1783
- warn('$[ident] in unknown values will not be evaluated as property references in the future. Use ${[ident]}', index, 'DEPRECATED', 'property-in-unknown-value');
1904
+ warn('$property in unknown values will not be evaluated as property references in the future. Use ${property}', index, 'DEPRECATED', 'property-in-unknown-value');
1784
1905
  }
1785
1906
  quote.variableRegex = /@([\w-]+)|@{([\w-]+)}/g;
1786
1907
  quote.propRegex = /\$([\w-]+)|\${([\w-]+)}/g;
@@ -1889,7 +2010,17 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1889
2010
  }
1890
2011
  parserInput.restore();
1891
2012
 
1892
- e = entities.declarationCall.bind(this)() || cssKeyword() || entities.keyword() || entities.variable() || entities.mixinLookup()
2013
+ e = entities.declarationCall.bind(this)() || cssKeyword() || entities.keyword() || entities.variableCurly();
2014
+ if (!e) {
2015
+ const varIndex = parserInput.i;
2016
+ const bareVariable = entities.variable();
2017
+ if (bareVariable) {
2018
+ warnBareAtRuleVariable(varIndex);
2019
+ e = bareVariable;
2020
+ } else {
2021
+ e = entities.mixinLookup();
2022
+ }
2023
+ }
1893
2024
  if (e) {
1894
2025
  nodes.push(e);
1895
2026
  if (e.type === 'Variable' ||
@@ -1900,7 +2031,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1900
2031
  let closed = false;
1901
2032
  p = this.property();
1902
2033
  parserInput.save();
1903
- if (!p && syntaxOptions.queryInParens && parserInput.$re(/^[0-9a-z-]*\s*([<>]=|<=|>=|[<>]|=)/)) {
2034
+ if (!p && syntaxOptions.queryInParens && parserInput.$re(/^(?:[^()]|\([^()]*\))*\s*([<>]=|<=|>=|[<>]|=)/)) {
1904
2035
  parserInput.restore();
1905
2036
  p = this.condition();
1906
2037
 
@@ -1980,7 +2111,17 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1980
2111
  features[features.length - 1].noSpacing = false;
1981
2112
  }
1982
2113
  } else {
1983
- e = entities.variable() || entities.mixinLookup();
2114
+ e = entities.variableCurly();
2115
+ if (!e) {
2116
+ const varIndex = parserInput.i;
2117
+ const bareVariable = entities.variable();
2118
+ if (bareVariable) {
2119
+ warnBareAtRuleVariable(varIndex);
2120
+ e = bareVariable;
2121
+ } else {
2122
+ e = entities.mixinLookup();
2123
+ }
2124
+ }
1984
2125
  if (e) {
1985
2126
  features.push(e);
1986
2127
  if (!parserInput.$char(',')) { break; }
@@ -2094,8 +2235,24 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2094
2235
  return null;
2095
2236
  }
2096
2237
  },
2238
+ /**
2239
+ * An entity in a non-value at-rule position (an at-rule identifier,
2240
+ * name, or keyword-list item — e.g. the name in `@keyframes @foo`).
2241
+ * `@{foo}` interpolation is the supported form; a bare `@foo` still
2242
+ * resolves but is deprecated.
2243
+ */
2244
+ atRuleEntity: function () {
2245
+ const curly = this.entities.variableCurly();
2246
+ if (curly) { return curly; }
2247
+ const index = parserInput.i;
2248
+ const e = this.entity();
2249
+ if (e && e.type === 'Variable') {
2250
+ warnBareAtRuleVariable(index);
2251
+ }
2252
+ return e;
2253
+ },
2097
2254
  atruleUnknown: function (value, name, hasBlock) {
2098
- value = this.permissiveValue(/^[{;]/);
2255
+ value = this.permissiveValue(/^[{;]/, true);
2099
2256
  hasBlock = (parserInput.currentChar() === '{');
2100
2257
  if (!value) {
2101
2258
  if (!hasBlock && parserInput.currentChar() !== ';') {
@@ -2111,16 +2268,16 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2111
2268
  rules = this.blockRuleset();
2112
2269
  parserInput.save();
2113
2270
  if (!rules && !isRooted) {
2114
- value = this.entity();
2271
+ value = this.atRuleEntity();
2115
2272
  rules = this.blockRuleset();
2116
2273
  }
2117
2274
  if (!rules && !isRooted) {
2118
2275
  parserInput.restore();
2119
2276
  var e = [];
2120
- value = this.entity();
2277
+ value = this.atRuleEntity();
2121
2278
  while (parserInput.$char(',')) {
2122
2279
  e.push(value);
2123
- value = this.entity();
2280
+ value = this.atRuleEntity();
2124
2281
  }
2125
2282
  if (value && e.length > 0) {
2126
2283
  e.push(value);
@@ -2205,12 +2362,28 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2205
2362
  parserInput.commentStore.length = 0;
2206
2363
 
2207
2364
  if (hasIdentifier) {
2208
- value = this.entity();
2365
+ value = this.atRuleEntity();
2209
2366
  if (!value) {
2210
2367
  error(`expected ${name} identifier`);
2211
2368
  }
2369
+ if (nonVendorSpecificName === '@charset') {
2370
+ warnDynamicCharset(value, index);
2371
+ }
2212
2372
  } else if (hasExpression) {
2373
+ // `@namespace` may carry an interpolated `@{ns}` prefix (or a
2374
+ // deprecated bare `@ns`). Parse that prefix directly so `@{ns}`
2375
+ // is accepted here without treating value positions as
2376
+ // interpolation contexts, then read the namespace URL.
2377
+ let prefix = this.entities.variableCurly();
2378
+ if (!prefix && parserInput.peek(/^@@?[\w-]/)) {
2379
+ const prefixIndex = parserInput.i;
2380
+ prefix = this.entities.variable();
2381
+ if (prefix) { warnBareAtRuleVariable(prefixIndex); }
2382
+ }
2213
2383
  value = this.expression();
2384
+ if (prefix) {
2385
+ value = value ? new(tree.Expression)([prefix, ...value.value]) : prefix;
2386
+ }
2214
2387
  if (!value) {
2215
2388
  error(`expected ${name} expression`);
2216
2389
  }
@@ -2400,7 +2573,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2400
2573
  return condition || a;
2401
2574
  }
2402
2575
  },
2403
- condition: function (needsParens) {
2576
+ condition: function (needsParens, allowConditionOperands) {
2404
2577
  let result;
2405
2578
  let logical;
2406
2579
  let next;
@@ -2408,13 +2581,13 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2408
2581
  return parserInput.$str('or');
2409
2582
  }
2410
2583
 
2411
- result = this.conditionAnd(needsParens);
2584
+ result = this.conditionAnd(needsParens, allowConditionOperands);
2412
2585
  if (!result) {
2413
2586
  return ;
2414
2587
  }
2415
2588
  logical = or();
2416
2589
  if (logical) {
2417
- next = this.condition(needsParens);
2590
+ next = this.condition(needsParens, allowConditionOperands);
2418
2591
  if (next) {
2419
2592
  result = new(tree.Condition)(logical, result, next);
2420
2593
  } else {
@@ -2423,13 +2596,13 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2423
2596
  }
2424
2597
  return result;
2425
2598
  },
2426
- conditionAnd: function (needsParens) {
2599
+ conditionAnd: function (needsParens, allowConditionOperands) {
2427
2600
  let result;
2428
2601
  let logical;
2429
2602
  let next;
2430
2603
  const self = this;
2431
2604
  function insideCondition() {
2432
- const cond = self.negatedCondition(needsParens) || self.parenthesisCondition(needsParens);
2605
+ const cond = self.negatedCondition(needsParens, allowConditionOperands) || self.parenthesisCondition(needsParens, allowConditionOperands);
2433
2606
  if (!cond && !needsParens) {
2434
2607
  return self.atomicCondition(needsParens);
2435
2608
  }
@@ -2443,9 +2616,12 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2443
2616
  if (!result) {
2444
2617
  return ;
2445
2618
  }
2619
+ if (allowConditionOperands) {
2620
+ result = this.atomicCondition(needsParens, result, allowConditionOperands) || result;
2621
+ }
2446
2622
  logical = and();
2447
2623
  if (logical) {
2448
- next = this.conditionAnd(needsParens);
2624
+ next = this.conditionAnd(needsParens, allowConditionOperands);
2449
2625
  if (next) {
2450
2626
  result = new(tree.Condition)(logical, result, next);
2451
2627
  } else {
@@ -2454,9 +2630,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2454
2630
  }
2455
2631
  return result;
2456
2632
  },
2457
- negatedCondition: function (needsParens) {
2633
+ negatedCondition: function (needsParens, allowConditionOperands) {
2458
2634
  if (parserInput.$str('not')) {
2459
- const result = this.parenthesisCondition(needsParens);
2635
+ const result = this.parenthesisCondition(needsParens, allowConditionOperands);
2460
2636
  if (result) {
2461
2637
  result.negate = !result.negate;
2462
2638
  return result;
@@ -2473,11 +2649,11 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2473
2649
  }
2474
2650
  }
2475
2651
  },
2476
- parenthesisCondition: function (needsParens) {
2652
+ parenthesisCondition: function (needsParens, allowConditionOperands) {
2477
2653
  function tryConditionFollowedByParenthesis(me) {
2478
2654
  let body;
2479
2655
  parserInput.save();
2480
- body = me.condition(needsParens);
2656
+ body = me.condition(needsParens, allowConditionOperands);
2481
2657
  if (!body) {
2482
2658
  parserInput.restore();
2483
2659
  return ;
@@ -2514,7 +2690,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2514
2690
  parserInput.forget();
2515
2691
  return body;
2516
2692
  },
2517
- atomicCondition: function (needsParens, preparsedCond) {
2693
+ atomicCondition: function (needsParens, preparsedCond, allowConditionOperands) {
2518
2694
  const entities = this.entities;
2519
2695
  const index = parserInput.i;
2520
2696
  let a;
@@ -2523,7 +2699,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2523
2699
  let op;
2524
2700
 
2525
2701
  const cond = (function() {
2526
- return this.addition() || entities.keyword() || entities.quoted() || entities.mixinLookup();
2702
+ return (allowConditionOperands && this.parenthesisCondition(needsParens)) || this.addition() || entities.keyword() || entities.quoted() || entities.mixinLookup();
2527
2703
  }).bind(this)
2528
2704
 
2529
2705
  if (preparsedCond) {
@@ -2684,6 +2860,11 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2684
2860
  }
2685
2861
  for (k = 0; k < name.length; k++) {
2686
2862
  s = name[k];
2863
+ if (s.charAt(0) === '@') {
2864
+ const variableName = s.slice(2, -1);
2865
+ warnNumericVariableName(variableName, index[k]);
2866
+ warnDashOnlyVariableName(variableName, index[k]);
2867
+ }
2687
2868
  name[k] = (s.charAt(0) !== '@' && s.charAt(0) !== '$') ?
2688
2869
  new(tree.Keyword)(s) :
2689
2870
  (s.charAt(0) === '@' ?
@@ -267,35 +267,47 @@ class Definition extends Ruleset {
267
267
  */
268
268
  matchArgs(args, context) {
269
269
  const allArgsCnt = (args && args.length) || 0;
270
- let len;
271
- const optionalParameters = this.optionalParameters;
272
- const requiredArgsCnt = !args ? 0 : args.reduce(function (/** @type {number} */ count, /** @type {MixinArg} */ p) {
273
- if (optionalParameters.indexOf(p.name) < 0) {
274
- return count + 1;
275
- } else {
276
- return count;
270
+ const evaldArguments = new Array(this.params.length);
271
+ const positionalArgs = [];
272
+ let positionalIndex = 0;
273
+
274
+ for (let i = 0; i < allArgsCnt; i++) {
275
+ const arg = /** @type {MixinArg[]} */ (args)[i];
276
+ if (!arg.name) {
277
+ positionalArgs.push(arg);
278
+ continue;
277
279
  }
278
- }, 0);
279
280
 
280
- if (!this.variadic) {
281
- if (requiredArgsCnt < this.required) {
281
+ const paramIndex = this.params.findIndex((param, index) => param.name === arg.name && !evaldArguments[index]);
282
+ if (paramIndex < 0) {
282
283
  return false;
283
284
  }
284
- if (allArgsCnt > this.params.length) {
285
- return false;
285
+ evaldArguments[paramIndex] = arg;
286
+ }
287
+
288
+ for (let i = 0; i < this.params.length; i++) {
289
+ if (evaldArguments[i]) {
290
+ continue;
291
+ }
292
+ if (this.params[i].variadic) {
293
+ positionalIndex = positionalArgs.length;
294
+ continue;
286
295
  }
287
- } else {
288
- if (requiredArgsCnt < (this.required - 1)) {
296
+ if (positionalIndex < positionalArgs.length) {
297
+ evaldArguments[i] = positionalArgs[positionalIndex++];
298
+ } else if (!this.params[i].name || !this.params[i].value) {
289
299
  return false;
290
300
  }
291
301
  }
292
302
 
293
- // check patterns
294
- len = Math.min(requiredArgsCnt, this.arity);
303
+ if (positionalIndex < positionalArgs.length) {
304
+ return false;
305
+ }
295
306
 
296
- for (let i = 0; i < len; i++) {
307
+ // check patterns
308
+ for (let i = 0; i < this.arity; i++) {
297
309
  if (!this.params[i].name && !this.params[i].variadic) {
298
- if (/** @type {MixinArg[]} */ (args)[i].value.eval(context).toCSS(/** @type {EvalContext} */ ({})) != /** @type {Node} */ (this.params[i].value).eval(context).toCSS(/** @type {EvalContext} */ ({}))) {
310
+ if (evaldArguments[i].value.eval(context).toCSS(/** @type {EvalContext} */ ({})) != /** @type {Node} */ (this.params[i].value).eval(context).toCSS(/** @type {EvalContext} */ ({}))) {
299
311
  return false;
300
312
  }
301
313
  }
@@ -145,16 +145,16 @@ const NestableAtRulePrototype = {
145
145
  self.features = new Value(self.permute(/** @type {Node[][]} */ (/** @type {unknown} */ (path))).map(
146
146
  /** @param {Node | Node[]} path */
147
147
  path => {
148
- path = /** @type {Node[]} */ (path).map(
148
+ path = /** @type {Node[]} */ (path).map(
149
149
  /** @param {Node & { toCSS?: Function }} fragment */
150
- fragment => fragment.toCSS ? fragment : new Anonymous(/** @type {string} */ (/** @type {unknown} */ (fragment))));
150
+ fragment => fragment.toCSS ? fragment : new Anonymous(/** @type {string} */ (/** @type {unknown} */ (fragment))));
151
151
 
152
- for (i = /** @type {Node[]} */ (path).length - 1; i > 0; i--) {
152
+ for (i = /** @type {Node[]} */ (path).length - 1; i > 0; i--) {
153
153
  /** @type {Node[]} */ (path).splice(i, 0, new Anonymous('and'));
154
- }
154
+ }
155
155
 
156
- return new Expression(/** @type {Node[]} */ (path));
157
- }));
156
+ return new Expression(/** @type {Node[]} */ (path));
157
+ }));
158
158
  self.setParent(self.features, self);
159
159
 
160
160
  // Fake a tree-node that doesn't output anything.