less 4.7.0 → 4.8.1

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.
@@ -22,6 +22,18 @@ const deprecations = {
22
22
  'variable-in-at-rule-prelude': {
23
23
  description: 'A bare @variable in an at-rule prelude (e.g. @media @foo) is deprecated. Use @{variable} interpolation instead.'
24
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.'
36
+ },
25
37
  'property-in-unknown-value': {
26
38
  description: '$property in custom property values is treated as literal text.'
27
39
  },
@@ -1,7 +1,14 @@
1
1
  import Dimension from '../tree/dimension.js';
2
2
 
3
- const MathHelper = (fn, unit, n) => {
3
+ const MathHelper = (fn, unit, cssEvaluable, n) => {
4
4
  if (!(n instanceof Dimension)) {
5
+ // A runtime CSS value such as var()/env() stays an unevaluated call and
6
+ // cannot be resolved to a number at compile time. Only functions with a
7
+ // CSS equivalent may be left for the browser; the rest (percentage, ceil,
8
+ // floor, round) have no CSS form and keep erroring on such input.
9
+ if (cssEvaluable && n && n.type === 'Call') {
10
+ return undefined;
11
+ }
5
12
  throw { type: 'Argument', message: 'argument must be a number' };
6
13
  }
7
14
  if (unit === null) {
@@ -1,29 +1,30 @@
1
1
  import mathHelper from './math-helper.js';
2
2
 
3
3
  const mathFunctions = {
4
- // name, unit
5
- ceil: null,
6
- floor: null,
7
- sqrt: null,
8
- abs: null,
9
- tan: '',
10
- sin: '',
11
- cos: '',
12
- atan: 'rad',
13
- asin: 'rad',
14
- acos: 'rad'
4
+ // name: [unit, has a CSS equivalent that the browser can evaluate]
5
+ ceil: [null, false],
6
+ floor: [null, false],
7
+ sqrt: [null, true],
8
+ abs: [null, true],
9
+ tan: ['', true],
10
+ sin: ['', true],
11
+ cos: ['', true],
12
+ atan: ['rad', true],
13
+ asin: ['rad', true],
14
+ acos: ['rad', true]
15
15
  };
16
16
 
17
17
  for (const f in mathFunctions) {
18
18
  // eslint-disable-next-line no-prototype-builtins
19
19
  if (mathFunctions.hasOwnProperty(f)) {
20
- mathFunctions[f] = mathHelper.bind(null, Math[f], mathFunctions[f]);
20
+ const [unit, cssEvaluable] = mathFunctions[f];
21
+ mathFunctions[f] = mathHelper.bind(null, Math[f], unit, cssEvaluable);
21
22
  }
22
23
  }
23
24
 
24
25
  mathFunctions.round = (n, f) => {
25
26
  const fraction = typeof f === 'undefined' ? 0 : f.value;
26
- return mathHelper(num => num.toFixed(fraction), null, n);
27
+ return mathHelper(num => num.toFixed(fraction), null, false, n);
27
28
  };
28
29
 
29
30
  export default mathFunctions;
@@ -88,7 +88,7 @@ export default {
88
88
  return new Dimension(Math.pow(x.value, y.value), x.unit);
89
89
  },
90
90
  percentage: function (n) {
91
- const result = mathHelper(num => num * 100, '%', n);
91
+ const result = mathHelper(num => num * 100, '%', false, n);
92
92
 
93
93
  return result;
94
94
  }
@@ -106,6 +106,64 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
106
106
  warn('A bare @variable in an at-rule prelude is deprecated. Use @{variable} interpolation instead.', index, 'DEPRECATED', 'variable-in-at-rule-prelude');
107
107
  }
108
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
+
109
167
  function expect(arg, msg) {
110
168
  // some older browsers return typeof 'function' for RegExp
111
169
  const result = (arg instanceof Function) ? arg.call(parsers) : parserInput.$re(arg);
@@ -559,7 +617,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
559
617
  }
560
618
 
561
619
  function condition() {
562
- return [expect(parsers.condition, 'expected condition')];
620
+ return [expect(() => parsers.condition(false, true), 'expected condition')];
563
621
  }
564
622
  },
565
623
 
@@ -684,6 +742,8 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
684
742
 
685
743
  parserInput.save();
686
744
  if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^@@?[\w-]+/))) {
745
+ warnNumericVariableName(name, index);
746
+ warnDashOnlyVariableName(name, index);
687
747
  ch = parserInput.currentChar();
688
748
  if ((ch === '(' && !parserInput.prevChar().match(/^\s/))
689
749
  || (ch === '[' && !parserInput.prevChar().match(/^\s/))) {
@@ -706,6 +766,8 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
706
766
  const index = parserInput.i;
707
767
 
708
768
  if (parserInput.currentChar() === '@' && (curly = parserInput.$re(/^@\{([\w-]+)\}/))) {
769
+ warnNumericVariableName(curly[1], index);
770
+ warnDashOnlyVariableName(curly[1], index);
709
771
  return new(tree.Variable)(`@${curly[1]}`, index + currentIndex, fileInfo);
710
772
  }
711
773
  },
@@ -828,7 +890,11 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
828
890
  variable: function () {
829
891
  let name;
830
892
 
831
- 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
+ }
832
898
  },
833
899
 
834
900
  //
@@ -858,6 +924,8 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
858
924
  }
859
925
 
860
926
  if (!inValue) {
927
+ warnNumericVariableName(name[1], i);
928
+ warnDashOnlyVariableName(name[1], i);
861
929
  name = name[1];
862
930
  }
863
931
 
@@ -1014,6 +1082,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1014
1082
  if (inValue || parsers.end()) {
1015
1083
  parserInput.forget();
1016
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
+ }
1017
1088
  if (lookups) {
1018
1089
  return new tree.NamespaceValue(mixin, lookups);
1019
1090
  }
@@ -1210,6 +1281,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1210
1281
  let ruleset;
1211
1282
  let cond;
1212
1283
  let variadic = false;
1284
+ const index = parserInput.i;
1213
1285
  if ((parserInput.currentChar() !== '.' && parserInput.currentChar() !== '#') ||
1214
1286
  parserInput.peek(/^[^{]*\}/)) {
1215
1287
  return;
@@ -1245,6 +1317,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1245
1317
 
1246
1318
  if (ruleset) {
1247
1319
  parserInput.forget();
1320
+ warnDashOnlyMixinName(name, index);
1248
1321
  return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);
1249
1322
  } else {
1250
1323
  parserInput.restore();
@@ -2293,6 +2366,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2293
2366
  if (!value) {
2294
2367
  error(`expected ${name} identifier`);
2295
2368
  }
2369
+ if (nonVendorSpecificName === '@charset') {
2370
+ warnDynamicCharset(value, index);
2371
+ }
2296
2372
  } else if (hasExpression) {
2297
2373
  // `@namespace` may carry an interpolated `@{ns}` prefix (or a
2298
2374
  // deprecated bare `@ns`). Parse that prefix directly so `@{ns}`
@@ -2497,7 +2573,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2497
2573
  return condition || a;
2498
2574
  }
2499
2575
  },
2500
- condition: function (needsParens) {
2576
+ condition: function (needsParens, allowConditionOperands) {
2501
2577
  let result;
2502
2578
  let logical;
2503
2579
  let next;
@@ -2505,13 +2581,13 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2505
2581
  return parserInput.$str('or');
2506
2582
  }
2507
2583
 
2508
- result = this.conditionAnd(needsParens);
2584
+ result = this.conditionAnd(needsParens, allowConditionOperands);
2509
2585
  if (!result) {
2510
2586
  return ;
2511
2587
  }
2512
2588
  logical = or();
2513
2589
  if (logical) {
2514
- next = this.condition(needsParens);
2590
+ next = this.condition(needsParens, allowConditionOperands);
2515
2591
  if (next) {
2516
2592
  result = new(tree.Condition)(logical, result, next);
2517
2593
  } else {
@@ -2520,13 +2596,13 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2520
2596
  }
2521
2597
  return result;
2522
2598
  },
2523
- conditionAnd: function (needsParens) {
2599
+ conditionAnd: function (needsParens, allowConditionOperands) {
2524
2600
  let result;
2525
2601
  let logical;
2526
2602
  let next;
2527
2603
  const self = this;
2528
2604
  function insideCondition() {
2529
- const cond = self.negatedCondition(needsParens) || self.parenthesisCondition(needsParens);
2605
+ const cond = self.negatedCondition(needsParens, allowConditionOperands) || self.parenthesisCondition(needsParens, allowConditionOperands);
2530
2606
  if (!cond && !needsParens) {
2531
2607
  return self.atomicCondition(needsParens);
2532
2608
  }
@@ -2540,9 +2616,12 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2540
2616
  if (!result) {
2541
2617
  return ;
2542
2618
  }
2619
+ if (allowConditionOperands) {
2620
+ result = this.atomicCondition(needsParens, result, allowConditionOperands) || result;
2621
+ }
2543
2622
  logical = and();
2544
2623
  if (logical) {
2545
- next = this.conditionAnd(needsParens);
2624
+ next = this.conditionAnd(needsParens, allowConditionOperands);
2546
2625
  if (next) {
2547
2626
  result = new(tree.Condition)(logical, result, next);
2548
2627
  } else {
@@ -2551,9 +2630,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2551
2630
  }
2552
2631
  return result;
2553
2632
  },
2554
- negatedCondition: function (needsParens) {
2633
+ negatedCondition: function (needsParens, allowConditionOperands) {
2555
2634
  if (parserInput.$str('not')) {
2556
- const result = this.parenthesisCondition(needsParens);
2635
+ const result = this.parenthesisCondition(needsParens, allowConditionOperands);
2557
2636
  if (result) {
2558
2637
  result.negate = !result.negate;
2559
2638
  return result;
@@ -2570,11 +2649,11 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2570
2649
  }
2571
2650
  }
2572
2651
  },
2573
- parenthesisCondition: function (needsParens) {
2652
+ parenthesisCondition: function (needsParens, allowConditionOperands) {
2574
2653
  function tryConditionFollowedByParenthesis(me) {
2575
2654
  let body;
2576
2655
  parserInput.save();
2577
- body = me.condition(needsParens);
2656
+ body = me.condition(needsParens, allowConditionOperands);
2578
2657
  if (!body) {
2579
2658
  parserInput.restore();
2580
2659
  return ;
@@ -2611,7 +2690,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2611
2690
  parserInput.forget();
2612
2691
  return body;
2613
2692
  },
2614
- atomicCondition: function (needsParens, preparsedCond) {
2693
+ atomicCondition: function (needsParens, preparsedCond, allowConditionOperands) {
2615
2694
  const entities = this.entities;
2616
2695
  const index = parserInput.i;
2617
2696
  let a;
@@ -2620,7 +2699,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2620
2699
  let op;
2621
2700
 
2622
2701
  const cond = (function() {
2623
- return this.addition() || entities.keyword() || entities.quoted() || entities.mixinLookup();
2702
+ return (allowConditionOperands && this.parenthesisCondition(needsParens)) || this.addition() || entities.keyword() || entities.quoted() || entities.mixinLookup();
2624
2703
  }).bind(this)
2625
2704
 
2626
2705
  if (preparsedCond) {
@@ -2781,6 +2860,11 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2781
2860
  }
2782
2861
  for (k = 0; k < name.length; k++) {
2783
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
+ }
2784
2868
  name[k] = (s.charAt(0) !== '@' && s.charAt(0) !== '$') ?
2785
2869
  new(tree.Keyword)(s) :
2786
2870
  (s.charAt(0) === '@' ?
@@ -151,6 +151,10 @@ class MixinCall extends Node {
151
151
  for (m = 0; m < expandedValues.length; m++) {
152
152
  args.push({value: expandedValues[m]});
153
153
  }
154
+ } else if (argValue.type === 'Expression' && Array.isArray(argValue.value) && argValue.value.length === 0) {
155
+ // an unset variadic holds no captured args; omit it so the callee
156
+ // falls through to its own parameter defaults instead of receiving empty
157
+
154
158
  } else {
155
159
  args.push({name: arg.name, value: argValue});
156
160
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "less",
3
- "version": "4.7.0",
3
+ "version": "4.8.1",
4
4
  "description": "Leaner CSS",
5
5
  "homepage": "http://lesscss.org",
6
6
  "author": {