less 4.6.7 → 4.7.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,13 @@ 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.'
21
24
  },
22
25
  'property-in-unknown-value': {
23
- description: '$[ident] in custom property values is treated as literal text.'
26
+ description: '$property in custom property values is treated as literal text.'
24
27
  },
25
28
  'js-eval': {
26
29
  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,17 @@ 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
+
89
109
  function expect(arg, msg) {
90
110
  // some older browsers return typeof 'function' for RegExp
91
111
  const result = (arg instanceof Function) ? arg.call(parsers) : parserInput.$re(arg);
@@ -1636,7 +1656,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1636
1656
  if (parserInput.$char(';')) {
1637
1657
  value = new Anonymous('');
1638
1658
  } else {
1639
- value = this.permissiveValue(/[;}]/, true);
1659
+ value = this.permissiveValue(/[;}]/);
1640
1660
  }
1641
1661
  }
1642
1662
  // Try to store values as anonymous
@@ -1695,8 +1715,12 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1695
1715
  * math is allowed.
1696
1716
  *
1697
1717
  * @param {RexExp} untilTokens - Characters to stop parsing at
1718
+ * @param {boolean} [deprecateVariables] - when set, this is an at-rule
1719
+ * prelude (non-value position); accept `@{var}` interpolation and warn
1720
+ * on a bare `@var` reference (which resolves today but is deprecated).
1698
1721
  */
1699
- permissiveValue: function (untilTokens) {
1722
+ permissiveValue: function (untilTokens, deprecateVariables) {
1723
+ const entities = this.entities;
1700
1724
  let i;
1701
1725
  let e;
1702
1726
  let done;
@@ -1723,7 +1747,20 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1723
1747
  value.push(e);
1724
1748
  continue;
1725
1749
  }
1726
- e = this.entity();
1750
+ if (deprecateVariables) {
1751
+ // In an at-rule prelude, `@{var}` interpolation is the supported
1752
+ // form; consume it here so its `{` is not mistaken for a block.
1753
+ e = entities.variableCurly();
1754
+ if (!e) {
1755
+ const varIndex = parserInput.i;
1756
+ e = this.entity();
1757
+ if (e && e.type === 'Variable') {
1758
+ warnBareAtRuleVariable(varIndex);
1759
+ }
1760
+ }
1761
+ } else {
1762
+ e = this.entity();
1763
+ }
1727
1764
  if (e) {
1728
1765
  value.push(e);
1729
1766
  }
@@ -1750,7 +1787,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1750
1787
  }
1751
1788
  parserInput.save();
1752
1789
 
1753
- value = parserInput.$parseUntil(tok);
1790
+ value = parserInput.$parseUntil(tok, deprecateVariables);
1754
1791
 
1755
1792
  if (value) {
1756
1793
  if (typeof value === 'string') {
@@ -1760,6 +1797,14 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1760
1797
  parserInput.forget();
1761
1798
  return new tree.Anonymous('', index);
1762
1799
  }
1800
+ // At-rule prelude: `$parseUntil` (deprecateVariables) records the
1801
+ // first bare `@var` it saw outside any `(...)` in its single pass —
1802
+ // a structural reference (`[...]`/`{...}` don't shield it, only a
1803
+ // declaration-value `(...)` does). Warn once here rather than
1804
+ // re-scanning the text.
1805
+ if (deprecateVariables && value.bareVarIndex !== null && value.bareVarIndex !== undefined) {
1806
+ warnBareAtRuleVariable(value.bareVarIndex);
1807
+ }
1763
1808
  /** @type {string} */
1764
1809
  let item;
1765
1810
  for (i = 0; i < value.length; i++) {
@@ -1776,11 +1821,14 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1776
1821
  const quote = new tree.Quoted('\'', item, true, index, fileInfo);
1777
1822
  const variableRegex = /@([\w-]+)/g;
1778
1823
  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');
1824
+ // At-rule preludes are handled once above via
1825
+ // `value.bareVarIndex`; the `variable-in-unknown-value`
1826
+ // notice is for unknown declaration values only.
1827
+ if (!deprecateVariables && variableRegex.test(item)) {
1828
+ warn('@variable in unknown values will not be evaluated as variables in the future. Use @{variable}', index, 'DEPRECATED', 'variable-in-unknown-value');
1781
1829
  }
1782
1830
  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');
1831
+ warn('$property in unknown values will not be evaluated as property references in the future. Use ${property}', index, 'DEPRECATED', 'property-in-unknown-value');
1784
1832
  }
1785
1833
  quote.variableRegex = /@([\w-]+)|@{([\w-]+)}/g;
1786
1834
  quote.propRegex = /\$([\w-]+)|\${([\w-]+)}/g;
@@ -1889,7 +1937,17 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1889
1937
  }
1890
1938
  parserInput.restore();
1891
1939
 
1892
- e = entities.declarationCall.bind(this)() || cssKeyword() || entities.keyword() || entities.variable() || entities.mixinLookup()
1940
+ e = entities.declarationCall.bind(this)() || cssKeyword() || entities.keyword() || entities.variableCurly();
1941
+ if (!e) {
1942
+ const varIndex = parserInput.i;
1943
+ const bareVariable = entities.variable();
1944
+ if (bareVariable) {
1945
+ warnBareAtRuleVariable(varIndex);
1946
+ e = bareVariable;
1947
+ } else {
1948
+ e = entities.mixinLookup();
1949
+ }
1950
+ }
1893
1951
  if (e) {
1894
1952
  nodes.push(e);
1895
1953
  if (e.type === 'Variable' ||
@@ -1900,7 +1958,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1900
1958
  let closed = false;
1901
1959
  p = this.property();
1902
1960
  parserInput.save();
1903
- if (!p && syntaxOptions.queryInParens && parserInput.$re(/^[0-9a-z-]*\s*([<>]=|<=|>=|[<>]|=)/)) {
1961
+ if (!p && syntaxOptions.queryInParens && parserInput.$re(/^(?:[^()]|\([^()]*\))*\s*([<>]=|<=|>=|[<>]|=)/)) {
1904
1962
  parserInput.restore();
1905
1963
  p = this.condition();
1906
1964
 
@@ -1980,7 +2038,17 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
1980
2038
  features[features.length - 1].noSpacing = false;
1981
2039
  }
1982
2040
  } else {
1983
- e = entities.variable() || entities.mixinLookup();
2041
+ e = entities.variableCurly();
2042
+ if (!e) {
2043
+ const varIndex = parserInput.i;
2044
+ const bareVariable = entities.variable();
2045
+ if (bareVariable) {
2046
+ warnBareAtRuleVariable(varIndex);
2047
+ e = bareVariable;
2048
+ } else {
2049
+ e = entities.mixinLookup();
2050
+ }
2051
+ }
1984
2052
  if (e) {
1985
2053
  features.push(e);
1986
2054
  if (!parserInput.$char(',')) { break; }
@@ -2094,8 +2162,24 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2094
2162
  return null;
2095
2163
  }
2096
2164
  },
2165
+ /**
2166
+ * An entity in a non-value at-rule position (an at-rule identifier,
2167
+ * name, or keyword-list item — e.g. the name in `@keyframes @foo`).
2168
+ * `@{foo}` interpolation is the supported form; a bare `@foo` still
2169
+ * resolves but is deprecated.
2170
+ */
2171
+ atRuleEntity: function () {
2172
+ const curly = this.entities.variableCurly();
2173
+ if (curly) { return curly; }
2174
+ const index = parserInput.i;
2175
+ const e = this.entity();
2176
+ if (e && e.type === 'Variable') {
2177
+ warnBareAtRuleVariable(index);
2178
+ }
2179
+ return e;
2180
+ },
2097
2181
  atruleUnknown: function (value, name, hasBlock) {
2098
- value = this.permissiveValue(/^[{;]/);
2182
+ value = this.permissiveValue(/^[{;]/, true);
2099
2183
  hasBlock = (parserInput.currentChar() === '{');
2100
2184
  if (!value) {
2101
2185
  if (!hasBlock && parserInput.currentChar() !== ';') {
@@ -2111,16 +2195,16 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2111
2195
  rules = this.blockRuleset();
2112
2196
  parserInput.save();
2113
2197
  if (!rules && !isRooted) {
2114
- value = this.entity();
2198
+ value = this.atRuleEntity();
2115
2199
  rules = this.blockRuleset();
2116
2200
  }
2117
2201
  if (!rules && !isRooted) {
2118
2202
  parserInput.restore();
2119
2203
  var e = [];
2120
- value = this.entity();
2204
+ value = this.atRuleEntity();
2121
2205
  while (parserInput.$char(',')) {
2122
2206
  e.push(value);
2123
- value = this.entity();
2207
+ value = this.atRuleEntity();
2124
2208
  }
2125
2209
  if (value && e.length > 0) {
2126
2210
  e.push(value);
@@ -2205,12 +2289,25 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
2205
2289
  parserInput.commentStore.length = 0;
2206
2290
 
2207
2291
  if (hasIdentifier) {
2208
- value = this.entity();
2292
+ value = this.atRuleEntity();
2209
2293
  if (!value) {
2210
2294
  error(`expected ${name} identifier`);
2211
2295
  }
2212
2296
  } else if (hasExpression) {
2297
+ // `@namespace` may carry an interpolated `@{ns}` prefix (or a
2298
+ // deprecated bare `@ns`). Parse that prefix directly so `@{ns}`
2299
+ // is accepted here without treating value positions as
2300
+ // interpolation contexts, then read the namespace URL.
2301
+ let prefix = this.entities.variableCurly();
2302
+ if (!prefix && parserInput.peek(/^@@?[\w-]/)) {
2303
+ const prefixIndex = parserInput.i;
2304
+ prefix = this.entities.variable();
2305
+ if (prefix) { warnBareAtRuleVariable(prefixIndex); }
2306
+ }
2213
2307
  value = this.expression();
2308
+ if (prefix) {
2309
+ value = value ? new(tree.Expression)([prefix, ...value.value]) : prefix;
2310
+ }
2214
2311
  if (!value) {
2215
2312
  error(`expected ${name} expression`);
2216
2313
  }
@@ -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.
@@ -8,7 +8,7 @@ class SourceMapGeneratorFallback {
8
8
  toJSON(){
9
9
  return null;
10
10
  }
11
- };
11
+ }
12
12
 
13
13
  export default {
14
14
  encodeBase64: function encodeBase64(str) {
@@ -19,9 +19,9 @@ export default {
19
19
  mimeLookup: function (filename) {
20
20
  try {
21
21
  const mimeModule = require('mime');
22
- return mimeModule ? mimeModule.lookup(filename) : "application/octet-stream";
22
+ return mimeModule ? mimeModule.lookup(filename) : 'application/octet-stream';
23
23
  } catch (e) {
24
- return "application/octet-stream";
24
+ return 'application/octet-stream';
25
25
  }
26
26
  },
27
27
  charsetLookup: function (mime) {
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'fs';
1
2
  import { createRequire } from 'module';
2
3
  import Dimension from '../less/tree/dimension.js';
3
4
  import Expression from '../less/tree/expression.js';
@@ -33,27 +34,45 @@ export default environment => {
33
34
  throw fileSync.error;
34
35
  }
35
36
 
36
- const sizeOf = require('image-size');
37
- return sizeOf ? sizeOf(fileSync.filename) : {width: 0, height: 0};
37
+ let probe;
38
+ try {
39
+ probe = require('probe-image-size/sync');
40
+ } catch (_) {
41
+ return { width: 0, height: 0 };
42
+ }
43
+
44
+ const size = probe(readFileSync(fileSync.filename));
45
+
46
+ if (!size) {
47
+ throw {
48
+ type: 'File',
49
+ message: `Unrecognised image format for '${filePath}'`
50
+ };
51
+ }
52
+
53
+ return {
54
+ width: size.width,
55
+ height: size.height
56
+ };
38
57
  }
39
58
 
40
59
  const imageFunctions = {
41
- 'image-size': function(filePathNode) {
60
+ 'image-size': function (filePathNode) {
42
61
  const size = imageSize(this, filePathNode);
43
62
  return new Expression([
44
63
  new Dimension(size.width, 'px'),
45
64
  new Dimension(size.height, 'px')
46
65
  ]);
47
66
  },
48
- 'image-width': function(filePathNode) {
67
+ 'image-width': function (filePathNode) {
49
68
  const size = imageSize(this, filePathNode);
50
69
  return new Dimension(size.width, 'px');
51
70
  },
52
- 'image-height': function(filePathNode) {
71
+ 'image-height': function (filePathNode) {
53
72
  const size = imageSize(this, filePathNode);
54
73
  return new Dimension(size.height, 'px');
55
74
  }
56
75
  };
57
76
 
58
77
  functionRegistry.addMultiple(imageFunctions);
59
- };
78
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "less",
3
- "version": "4.6.7",
3
+ "version": "4.7.0",
4
4
  "description": "Leaner CSS",
5
5
  "homepage": "http://lesscss.org",
6
6
  "author": {
@@ -70,7 +70,7 @@
70
70
  "optionalDependencies": {
71
71
  "errno": "^0.1.1",
72
72
  "graceful-fs": "^4.1.2",
73
- "image-size": "~0.5.0",
73
+ "probe-image-size": "^7.2.3",
74
74
  "make-dir": "^5.1.0",
75
75
  "mime": "^1.4.1",
76
76
  "needle": "^3.1.0",