less 4.9.0 → 5.0.0-alpha.2

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.
Files changed (131) hide show
  1. package/README.md +19 -5
  2. package/bin/lessc +190 -630
  3. package/dist/less-node.cjs +399 -14570
  4. package/index.cjs +5 -2
  5. package/lib/index.js +164 -0
  6. package/lib/lessc-helper.js +52 -0
  7. package/lib/logger.js +76 -0
  8. package/lib/options.d.ts +46 -0
  9. package/lib/options.js +145 -0
  10. package/lib/types.js +20 -0
  11. package/lib/version.js +19 -0
  12. package/package.json +32 -39
  13. package/dist/less.cjs +0 -15215
  14. package/dist/less.js +0 -15215
  15. package/dist/less.min.js +0 -11
  16. package/dist/less.min.js.map +0 -1
  17. package/lib/less/constants.js +0 -13
  18. package/lib/less/contexts.js +0 -165
  19. package/lib/less/data/colors.js +0 -150
  20. package/lib/less/data/index.js +0 -4
  21. package/lib/less/data/unit-conversions.js +0 -21
  22. package/lib/less/default-options.js +0 -89
  23. package/lib/less/deprecation.js +0 -82
  24. package/lib/less/environment/abstract-file-manager.js +0 -140
  25. package/lib/less/environment/abstract-plugin-loader.js +0 -185
  26. package/lib/less/environment/environment-api.ts +0 -21
  27. package/lib/less/environment/environment.js +0 -59
  28. package/lib/less/environment/file-manager-api.ts +0 -77
  29. package/lib/less/functions/boolean.js +0 -29
  30. package/lib/less/functions/color-blending.js +0 -85
  31. package/lib/less/functions/color.js +0 -452
  32. package/lib/less/functions/data-uri.js +0 -74
  33. package/lib/less/functions/default.js +0 -26
  34. package/lib/less/functions/function-caller.js +0 -55
  35. package/lib/less/functions/function-registry.js +0 -36
  36. package/lib/less/functions/index.js +0 -35
  37. package/lib/less/functions/list.js +0 -158
  38. package/lib/less/functions/math-helper.js +0 -22
  39. package/lib/less/functions/math.js +0 -30
  40. package/lib/less/functions/number.js +0 -95
  41. package/lib/less/functions/string.js +0 -39
  42. package/lib/less/functions/style.js +0 -29
  43. package/lib/less/functions/svg.js +0 -87
  44. package/lib/less/functions/types.js +0 -70
  45. package/lib/less/import-manager.js +0 -183
  46. package/lib/less/index.js +0 -95
  47. package/lib/less/less-error.js +0 -166
  48. package/lib/less/logger.js +0 -34
  49. package/lib/less/parse-tree.js +0 -124
  50. package/lib/less/parse.js +0 -87
  51. package/lib/less/parser/lookup-pattern.js +0 -26
  52. package/lib/less/parser/parser-input.js +0 -400
  53. package/lib/less/parser/parser.js +0 -2998
  54. package/lib/less/plugin-manager.js +0 -168
  55. package/lib/less/render.js +0 -41
  56. package/lib/less/source-map-builder.js +0 -82
  57. package/lib/less/source-map-output.js +0 -151
  58. package/lib/less/transform-tree.js +0 -102
  59. package/lib/less/tree/anonymous.js +0 -61
  60. package/lib/less/tree/assignment.js +0 -48
  61. package/lib/less/tree/atrule-syntax.js +0 -8
  62. package/lib/less/tree/atrule.js +0 -340
  63. package/lib/less/tree/attribute.js +0 -63
  64. package/lib/less/tree/call.js +0 -132
  65. package/lib/less/tree/color.js +0 -285
  66. package/lib/less/tree/combinator.js +0 -38
  67. package/lib/less/tree/comment.js +0 -48
  68. package/lib/less/tree/condition.js +0 -65
  69. package/lib/less/tree/container.js +0 -104
  70. package/lib/less/tree/debug-info.js +0 -80
  71. package/lib/less/tree/declaration.js +0 -139
  72. package/lib/less/tree/detached-ruleset.js +0 -45
  73. package/lib/less/tree/dimension.js +0 -215
  74. package/lib/less/tree/element.js +0 -93
  75. package/lib/less/tree/expression.js +0 -100
  76. package/lib/less/tree/extend.js +0 -88
  77. package/lib/less/tree/import.js +0 -299
  78. package/lib/less/tree/index.js +0 -56
  79. package/lib/less/tree/interpolated-variable.js +0 -106
  80. package/lib/less/tree/javascript.js +0 -45
  81. package/lib/less/tree/js-eval-node.js +0 -80
  82. package/lib/less/tree/keyword.js +0 -28
  83. package/lib/less/tree/media.js +0 -90
  84. package/lib/less/tree/merge-rules.js +0 -47
  85. package/lib/less/tree/mixin-call.js +0 -283
  86. package/lib/less/tree/mixin-definition.js +0 -319
  87. package/lib/less/tree/namespace-value.js +0 -95
  88. package/lib/less/tree/negative.js +0 -37
  89. package/lib/less/tree/nested-at-rule.js +0 -239
  90. package/lib/less/tree/node.js +0 -310
  91. package/lib/less/tree/operation.js +0 -81
  92. package/lib/less/tree/paren.js +0 -41
  93. package/lib/less/tree/property.js +0 -96
  94. package/lib/less/tree/query-in-parens.js +0 -64
  95. package/lib/less/tree/quoted.js +0 -131
  96. package/lib/less/tree/ruleset.js +0 -1068
  97. package/lib/less/tree/selector.js +0 -201
  98. package/lib/less/tree/unicode-descriptor.js +0 -14
  99. package/lib/less/tree/unit.js +0 -170
  100. package/lib/less/tree/url.js +0 -83
  101. package/lib/less/tree/value.js +0 -60
  102. package/lib/less/tree/variable-call.js +0 -60
  103. package/lib/less/tree/variable.js +0 -85
  104. package/lib/less/utils.js +0 -126
  105. package/lib/less/visitors/extend-visitor.js +0 -505
  106. package/lib/less/visitors/import-sequencer.js +0 -56
  107. package/lib/less/visitors/import-visitor.js +0 -203
  108. package/lib/less/visitors/index.js +0 -15
  109. package/lib/less/visitors/join-selector-visitor.js +0 -65
  110. package/lib/less/visitors/set-tree-visibility-visitor.js +0 -57
  111. package/lib/less/visitors/to-css-visitor.js +0 -332
  112. package/lib/less/visitors/visitor.js +0 -165
  113. package/lib/less-browser/add-default-options.js +0 -53
  114. package/lib/less-browser/bootstrap.js +0 -67
  115. package/lib/less-browser/browser.js +0 -65
  116. package/lib/less-browser/cache.js +0 -43
  117. package/lib/less-browser/error-reporting.js +0 -170
  118. package/lib/less-browser/file-manager.js +0 -112
  119. package/lib/less-browser/image-size.js +0 -28
  120. package/lib/less-browser/index.js +0 -284
  121. package/lib/less-browser/log-listener.js +0 -42
  122. package/lib/less-browser/plugin-loader.js +0 -24
  123. package/lib/less-browser/utils.js +0 -30
  124. package/lib/less-node/environment.js +0 -43
  125. package/lib/less-node/file-manager.js +0 -156
  126. package/lib/less-node/fs.js +0 -14
  127. package/lib/less-node/image-size.js +0 -78
  128. package/lib/less-node/index.js +0 -31
  129. package/lib/less-node/lessc-helper.js +0 -95
  130. package/lib/less-node/plugin-loader.js +0 -62
  131. package/lib/less-node/url-file-manager.js +0 -60
@@ -1,2998 +0,0 @@
1
- import LessError from '../less-error.js';
2
- import tree from '../tree/index.js';
3
- import visitors from '../visitors/index.js';
4
- import getParserInput from './parser-input.js';
5
- import * as utils from '../utils.js';
6
- import functionRegistry from '../functions/function-registry.js';
7
- import { ContainerSyntaxOptions, MediaSyntaxOptions } from '../tree/atrule-syntax.js';
8
- import logger from '../logger.js';
9
- import { DeprecationHandler } from '../deprecation.js';
10
- import Selector from '../tree/selector.js';
11
- import Anonymous from '../tree/anonymous.js';
12
- import { VARIABLE_WITH_LOOKUPS } from './lookup-pattern.js';
13
- import {
14
- splitLookups,
15
- resolveInterpolatedVariable,
16
- resolveInterpolatedProperty,
17
- hasInterpolation,
18
- VARIABLE_INTERPOLATION,
19
- PROPERTY_INTERPOLATION
20
- } from '../tree/interpolated-variable.js';
21
-
22
- /**
23
- * One particle of a property name: a literal chunk, an `@{...}` interpolation
24
- * which may carry a lookup chain, or a `${...}` interpolation which may not.
25
- *
26
- * The sigils are spelled out separately rather than sharing `[@$]`: properties
27
- * have no lookup grammar, so accepting `${name[key]}` here would hand `name[key]`
28
- * to `resolveInterpolatedProperty` and produce a misleading "undefined property"
29
- * error for syntax the language does not define.
30
- */
31
- const RULE_PROPERTY_PARTICLE = new RegExp(
32
- `^((?:[\\w-]+)|(?:@\\{${VARIABLE_WITH_LOOKUPS}\\})|(?:\\$\\{[\\w-]+\\}))`
33
- );
34
-
35
- //
36
- // less.js - parser
37
- //
38
- // A relatively straight-forward predictive parser.
39
- // There is no tokenization/lexing stage, the input is parsed
40
- // in one sweep.
41
- //
42
- // To make the parser fast enough to run in the browser, several
43
- // optimization had to be made:
44
- //
45
- // - Matching and slicing on a huge input is often cause of slowdowns.
46
- // The solution is to chunkify the input into smaller strings.
47
- // The chunks are stored in the `chunks` var,
48
- // `j` holds the current chunk index, and `currentPos` holds
49
- // the index of the current chunk in relation to `input`.
50
- // This gives us an almost 4x speed-up.
51
- //
52
- // - In many cases, we don't need to match individual tokens;
53
- // for example, if a value doesn't hold any variables, operations
54
- // or dynamic references, the parser can effectively 'skip' it,
55
- // treating it as a literal.
56
- // An example would be '1px solid #000' - which evaluates to itself,
57
- // we don't need to know what the individual components are.
58
- // The drawback, of course is that you don't get the benefits of
59
- // syntax-checking on the CSS. This gives us a 50% speed-up in the parser,
60
- // and a smaller speed-up in the code-gen.
61
- //
62
- //
63
- // Token matching is done with the `$` function, which either takes
64
- // a terminal string or regexp, or a non-terminal function to call.
65
- // It also takes care of moving all the indices forwards.
66
- //
67
-
68
- const Parser = function Parser(context, imports, fileInfo, currentIndex) {
69
- currentIndex = currentIndex || 0;
70
- let parsers;
71
- const parserInput = getParserInput();
72
-
73
- function error(msg, type) {
74
- throw new LessError(
75
- {
76
- index: parserInput.i,
77
- filename: fileInfo.filename,
78
- type: type || 'Syntax',
79
- message: msg
80
- },
81
- imports
82
- );
83
- }
84
-
85
- const deprecationHandler = new DeprecationHandler();
86
-
87
- // Tracks `${deprecationId}@${index}` pairs already warned about, so a source
88
- // position that gets re-parsed via parser backtracking only warns once.
89
- const warnedDeprecations = new Set();
90
-
91
- /**
92
- * @param {string} msg
93
- * @param {number} index
94
- * @param {string} type
95
- * @param {string} [deprecationId] - stable deprecation ID for repetition limiting
96
- */
97
- function warn(msg, index, type, deprecationId) {
98
- if (context.quiet) { return; }
99
- if (deprecationId && context.quietDeprecations) { return; }
100
- if (deprecationId) {
101
- const key = `${deprecationId}@${index ?? parserInput.i}`;
102
- if (warnedDeprecations.has(key)) { return; }
103
- warnedDeprecations.add(key);
104
- }
105
- if (deprecationId && !deprecationHandler.shouldWarn(deprecationId)) { return; }
106
-
107
- logger.warn(
108
- (new LessError(
109
- {
110
- index: index ?? parserInput.i,
111
- filename: fileInfo.filename,
112
- type: type ? `${type.toUpperCase()} WARNING` : 'WARNING',
113
- message: msg
114
- },
115
- imports
116
- )).toString()
117
- );
118
- }
119
-
120
- /**
121
- * Warn that a bare `@variable` reference is being used in a non-value
122
- * position (an at-rule prelude, name, or identifier), where it still
123
- * resolves today but is deprecated in favour of `@{variable}` interpolation.
124
- *
125
- * @param {number} index - source position of the bare reference
126
- */
127
- function warnBareAtRuleVariable(index) {
128
- warn('A bare @variable in an at-rule prelude is deprecated. Use @{variable} interpolation instead.', index, 'DEPRECATED', 'variable-in-at-rule-prelude');
129
- }
130
-
131
- /**
132
- * Whether a parsed entity is a bare `@variable` reference in a structural
133
- * position, and so subject to the interpolation deprecation.
134
- *
135
- * A lookup such as `@map[key]` is parsed by `entities.variable()` into a
136
- * `NamespaceValue` wrapping a `VariableCall`, not a `Variable`. Testing for
137
- * `Variable` alone silently exempted every bare lookup from the deprecation.
138
- *
139
- * @param {{ type?: string } | undefined | null} e
140
- * @returns {boolean}
141
- */
142
- function isBareVariableReference(e) {
143
- if (!e) { return false; }
144
- return e.type === 'Variable' || e.type === 'VariableCall' || e.type === 'NamespaceValue';
145
- }
146
-
147
- /**
148
- * Numeric-leading variable names are a Less extension rather than valid CSS
149
- * identifier syntax. Keep accepting them through Less 4, but make the Less 5
150
- * migration visible at every actual variable reference or definition.
151
- *
152
- * @param {string} name - either an @-prefixed name or the name inside @{...}
153
- * @param {number} index - source position of the variable token
154
- */
155
- function warnNumericVariableName(name, index) {
156
- if (/^(?:@@?)?[0-9]/.test(name)) {
157
- 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');
158
- }
159
- }
160
-
161
- /**
162
- * A lone '-' is not a CSS identifier. Less historically accepts it as a
163
- * variable name; retain that in Less 4 only and warn on definitions,
164
- * ordinary references, and interpolation references.
165
- *
166
- * @param {string} name - either @-/@@-, or the name inside @{...}
167
- * @param {number} index - source position of the variable token
168
- */
169
- function warnDashOnlyVariableName(name, index) {
170
- const bareName = name.replace(/^@@?/, '');
171
- if (bareName === '-') {
172
- 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');
173
- }
174
- }
175
-
176
- /**
177
- * A lone '-' is not a CSS identifier. Less historically accepts it as a mixin
178
- * name after '.' or '#'; retain that in Less 4 only.
179
- *
180
- * @param {string} name
181
- * @param {number} index
182
- */
183
- function warnDashOnlyMixinName(name, index) {
184
- if (name === '.-' || name === '#-') {
185
- 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');
186
- }
187
- }
188
-
189
- /**
190
- * CSS @charset is a source-header declaration, not a general dynamic at-rule.
191
- * Less 4 keeps its historical interpolation behavior for compatibility, but
192
- * makes each dynamic spelling visible before Less 5 rejects it.
193
- *
194
- * @param {import('../tree/node.js').default} value
195
- * @param {number} index - source position of the @charset token
196
- */
197
- function warnDynamicCharset(value, index) {
198
- if (value instanceof tree.Variable ||
199
- (value instanceof tree.Quoted &&
200
- (value.containsVariables() || value.value.match(value.propRegex)))) {
201
- 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');
202
- }
203
- }
204
-
205
- function expect(arg, msg) {
206
- // some older browsers return typeof 'function' for RegExp
207
- const result = (arg instanceof Function) ? arg.call(parsers) : parserInput.$re(arg);
208
- if (result) {
209
- return result;
210
- }
211
-
212
- error(msg || (typeof arg === 'string'
213
- ? `expected '${arg}' got '${parserInput.currentChar()}'`
214
- : 'unexpected token'));
215
- }
216
-
217
- // Specialization of expect()
218
- function expectChar(arg, msg) {
219
- if (parserInput.$char(arg)) {
220
- return arg;
221
- }
222
- error(msg || `expected '${arg}' got '${parserInput.currentChar()}'`);
223
- }
224
-
225
- function getDebugInfo(index) {
226
- const filename = fileInfo.filename;
227
-
228
- return {
229
- lineNumber: utils.getLocation(index, parserInput.getInput()).line + 1,
230
- fileName: filename
231
- };
232
- }
233
-
234
- /**
235
- * Used after initial parsing to create nodes on the fly
236
- *
237
- * @param {String} str - string to parse
238
- * @param {Array} parseList - array of parsers to run input through e.g. ["value", "important"]
239
- * @param {Number} currentIndex - start number to begin indexing
240
- * @param {Object} fileInfo - fileInfo to attach to created nodes
241
- */
242
- function parseNode(str, parseList, callback) {
243
- let result;
244
- const returnNodes = [];
245
- const parser = parserInput;
246
-
247
- try {
248
- parser.start(str);
249
- for (let x = 0, p; (p = parseList[x]); x++) {
250
- result = parsers[p]();
251
- returnNodes.push(result || null);
252
- }
253
-
254
- const endInfo = parser.end();
255
- if (endInfo.isFinished) {
256
- callback(null, returnNodes);
257
- }
258
- else {
259
- callback(true, null);
260
- }
261
- } catch (e) {
262
- throw new LessError({
263
- index: e.index + currentIndex,
264
- message: e.message
265
- }, imports, fileInfo.filename);
266
- }
267
- }
268
-
269
- //
270
- // The Parser
271
- //
272
- return {
273
- parserInput,
274
- imports,
275
- fileInfo,
276
- parseNode,
277
- //
278
- // Parse an input string into an abstract syntax tree,
279
- // @param str A string containing 'less' markup
280
- // @param callback call `callback` when done.
281
- // @param [additionalData] An optional map which can contains vars - a map (key, value) of variables to apply
282
- //
283
- parse: function (str, callback, additionalData) {
284
- let root;
285
- let err = null;
286
- let globalVars;
287
- let modifyVars;
288
- let ignored;
289
- let preText = '';
290
-
291
- // Optionally disable @plugin parsing
292
- if (additionalData && additionalData.disablePluginRule) {
293
- parsers.plugin = function() {
294
- var dir = parserInput.$re(/^@plugin?\s+/);
295
- if (dir) {
296
- error('@plugin statements are not allowed when disablePluginRule is set to true');
297
- }
298
- }
299
- }
300
-
301
- globalVars = (additionalData && additionalData.globalVars) ? `${Parser.serializeVars(additionalData.globalVars)}\n` : '';
302
- modifyVars = (additionalData && additionalData.modifyVars) ? `\n${Parser.serializeVars(additionalData.modifyVars)}` : '';
303
-
304
- if (context.pluginManager) {
305
- const preProcessors = context.pluginManager.getPreProcessors();
306
- for (let i = 0; i < preProcessors.length; i++) {
307
- str = preProcessors[i].process(str, { context, imports, fileInfo });
308
- }
309
- }
310
-
311
- if (globalVars || (additionalData && additionalData.banner)) {
312
- preText = ((additionalData && additionalData.banner) ? additionalData.banner : '') + globalVars;
313
- ignored = imports.contentsIgnoredChars;
314
- ignored[fileInfo.filename] = ignored[fileInfo.filename] || 0;
315
- ignored[fileInfo.filename] += preText.length;
316
- }
317
-
318
- str = str.replace(/\r\n?/g, '\n');
319
- // Remove potential UTF Byte Order Mark
320
- str = preText + str.replace(/^\uFEFF/, '') + modifyVars;
321
- imports.contents[fileInfo.filename] = str;
322
-
323
- // Start with the primary rule.
324
- // The whole syntax tree is held under a Ruleset node,
325
- // with the `root` property set to true, so no `{}` are
326
- // output. The callback is called when the input is parsed.
327
- try {
328
- parserInput.start(str);
329
-
330
- tree.Node.prototype.parse = this;
331
- root = new tree.Ruleset(null, this.parsers.primary());
332
- tree.Node.prototype.rootNode = root;
333
- root.root = true;
334
- root.firstRoot = true;
335
- root.functionRegistry = functionRegistry.inherit();
336
-
337
- } catch (e) {
338
- return callback(new LessError(e, imports, fileInfo.filename));
339
- }
340
-
341
- // If `i` is smaller than the `input.length - 1`,
342
- // it means the parser wasn't able to parse the whole
343
- // string, so we've got a parsing error.
344
- //
345
- // We try to extract a \n delimited string,
346
- // showing the line where the parse error occurred.
347
- // We split it up into two parts (the part which parsed,
348
- // and the part which didn't), so we can color them differently.
349
- const endInfo = parserInput.end();
350
- if (!endInfo.isFinished) {
351
-
352
- let message = endInfo.furthestPossibleErrorMessage;
353
-
354
- if (!message) {
355
- message = 'Unrecognised input';
356
- if (endInfo.furthestChar === '}') {
357
- message += '. Possibly missing opening \'{\'';
358
- } else if (endInfo.furthestChar === ')') {
359
- message += '. Possibly missing opening \'(\'';
360
- } else if (endInfo.furthestReachedEnd) {
361
- message += '. Possibly missing something';
362
- }
363
- }
364
-
365
- err = new LessError({
366
- type: 'Parse',
367
- message,
368
- index: endInfo.furthest,
369
- filename: fileInfo.filename
370
- }, imports);
371
- }
372
-
373
- const finish = e => {
374
- e = err || e || imports.error;
375
-
376
- if (e) {
377
- if (!(e instanceof LessError)) {
378
- e = new LessError(e, imports, fileInfo.filename);
379
- }
380
-
381
- return callback(e);
382
- }
383
- else {
384
- return callback(null, root);
385
- }
386
- };
387
-
388
- if (context.processImports !== false) {
389
- new visitors.ImportVisitor(imports, finish)
390
- .run(root);
391
- } else {
392
- return finish();
393
- }
394
- },
395
-
396
- //
397
- // Here in, the parsing rules/functions
398
- //
399
- // The basic structure of the syntax tree generated is as follows:
400
- //
401
- // Ruleset -> Declaration -> Value -> Expression -> Entity
402
- //
403
- // Here's some Less code:
404
- //
405
- // .class {
406
- // color: #fff;
407
- // border: 1px solid #000;
408
- // width: @w + 4px;
409
- // > .child {...}
410
- // }
411
- //
412
- // And here's what the parse tree might look like:
413
- //
414
- // Ruleset (Selector '.class', [
415
- // Declaration ("color", Value ([Expression [Color #fff]]))
416
- // Declaration ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
417
- // Declaration ("width", Value ([Expression [Operation " + " [Variable "@w"][Dimension 4px]]]))
418
- // Ruleset (Selector [Element '>', '.child'], [...])
419
- // ])
420
- //
421
- // In general, most rules will try to parse a token with the `$re()` function, and if the return
422
- // value is truly, will return a new node, of the relevant type. Sometimes, we need to check
423
- // first, before parsing, that's when we use `peek()`.
424
- //
425
- parsers: parsers = {
426
- //
427
- // The `primary` rule is the *entry* and *exit* point of the parser.
428
- // The rules here can appear at any level of the parse tree.
429
- //
430
- // The recursive nature of the grammar is an interplay between the `block`
431
- // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
432
- // as represented by this simplified grammar:
433
- //
434
- // primary → (ruleset | declaration)+
435
- // ruleset → selector+ block
436
- // block → '{' primary '}'
437
- //
438
- // Only at one point is the primary rule not called from the
439
- // block rule: at the root level.
440
- //
441
- primary: function () {
442
- const mixin = this.mixin;
443
- let root = [];
444
- let node;
445
-
446
- while (true) {
447
- while (true) {
448
- node = this.comment();
449
- if (!node) { break; }
450
- root.push(node);
451
- }
452
- // always process comments before deciding if finished
453
- if (parserInput.finished) {
454
- break;
455
- }
456
- if (parserInput.peek('}')) {
457
- break;
458
- }
459
-
460
- node = this.extendRule();
461
- if (node) {
462
- root = root.concat(node);
463
- continue;
464
- }
465
-
466
- node = mixin.definition() || this.declaration() || mixin.call(false, false) ||
467
- this.ruleset() || this.variableCall() || this.entities.call() || this.atrule();
468
- if (node) {
469
- root.push(node);
470
- } else {
471
- let foundSemiColon = false;
472
- while (parserInput.$char(';')) {
473
- foundSemiColon = true;
474
- }
475
- if (!foundSemiColon) {
476
- break;
477
- }
478
- }
479
- }
480
-
481
- return root;
482
- },
483
-
484
- // comments are collected by the main parsing mechanism and then assigned to nodes
485
- // where the current structure allows it
486
- comment: function () {
487
- if (parserInput.commentStore.length) {
488
- const comment = parserInput.commentStore.shift();
489
- return new(tree.Comment)(comment.text, comment.isLineComment, comment.index + currentIndex, fileInfo);
490
- }
491
- },
492
-
493
- //
494
- // Entities are tokens which can be found inside an Expression
495
- //
496
- entities: {
497
- mixinLookup: function() {
498
- return parsers.mixin.call(true, true);
499
- },
500
- //
501
- // A string, which supports escaping " and '
502
- //
503
- // "milky way" 'he\'s the one!'
504
- //
505
- quoted: function (forceEscaped) {
506
- let str;
507
- const index = parserInput.i;
508
- let isEscaped = false;
509
-
510
- parserInput.save();
511
- if (parserInput.$char('~')) {
512
- isEscaped = true;
513
- } else if (forceEscaped) {
514
- parserInput.restore();
515
- return;
516
- }
517
-
518
- str = parserInput.$quoted();
519
- if (!str) {
520
- parserInput.restore();
521
- return;
522
- }
523
- parserInput.forget();
524
-
525
- return new(tree.Quoted)(str.charAt(0), str.slice(1, -1), isEscaped, index + currentIndex, fileInfo);
526
- },
527
-
528
- //
529
- // A catch-all word, such as:
530
- //
531
- // black border-collapse
532
- //
533
- keyword: function () {
534
- const k = parserInput.$char('%') || parserInput.$re(/^\[?(?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+\]?/);
535
- if (k) {
536
- return tree.Color.fromKeyword(k) || new(tree.Keyword)(k);
537
- }
538
- },
539
-
540
- //
541
- // A function call
542
- //
543
- // rgb(255, 0, 255)
544
- //
545
- // The arguments are parsed with the `entities.arguments` parser.
546
- //
547
- call: function () {
548
- let name;
549
- let args;
550
- let func;
551
- const index = parserInput.i;
552
-
553
- // http://jsperf.com/case-insensitive-regex-vs-strtolower-then-regex/18
554
- if (parserInput.peek(/^url\(/i)) {
555
- return;
556
- }
557
-
558
- parserInput.save();
559
-
560
- name = parserInput.$re(/^([\w-]+|%|~|progid:[\w.]+)\(/);
561
- if (!name) {
562
- parserInput.forget();
563
- return;
564
- }
565
-
566
- name = name[1];
567
- func = this.customFuncCall(name);
568
- if (func) {
569
- args = func.parse();
570
- if (args && func.stop) {
571
- parserInput.forget();
572
- return args;
573
- }
574
- }
575
-
576
- args = this.arguments(args);
577
-
578
- if (!parserInput.$char(')')) {
579
- parserInput.restore('Could not parse call arguments or missing \')\'');
580
- return;
581
- }
582
-
583
- parserInput.forget();
584
-
585
- return new(tree.Call)(name, args, index + currentIndex, fileInfo);
586
- },
587
-
588
- declarationCall: function () {
589
- let validCall;
590
- let args;
591
- const index = parserInput.i;
592
-
593
- parserInput.save();
594
-
595
- validCall = parserInput.$re(/^[\w]+\(/);
596
- if (!validCall) {
597
- parserInput.forget();
598
- return;
599
- }
600
-
601
- validCall = validCall.substring(0, validCall.length - 1);
602
-
603
- // CSS at-rule keywords should never be parsed as declaration calls
604
- if (/^(and|or|not|only|layer)$/i.test(validCall)) {
605
- parserInput.restore();
606
- return;
607
- }
608
-
609
- let rule = this.ruleProperty();
610
- let value;
611
-
612
- if (rule) {
613
- value = this.value();
614
- }
615
-
616
- if (rule && value) {
617
- args = [new (tree.Declaration)(rule, value, null, null, parserInput.i + currentIndex, fileInfo, true)];
618
- }
619
-
620
- if (!parserInput.$char(')')) {
621
- parserInput.restore('Could not parse call arguments or missing \')\'');
622
- return;
623
- }
624
-
625
- parserInput.forget();
626
-
627
- return new(tree.Call)(validCall, args, index + currentIndex, fileInfo);
628
- },
629
-
630
- //
631
- // Parsing rules for functions with non-standard args, e.g.:
632
- //
633
- // boolean(not(2 > 1))
634
- //
635
- // This is a quick prototype, to be modified/improved when
636
- // more custom-parsed funcs come (e.g. `selector(...)`)
637
- //
638
-
639
- customFuncCall: function (name) {
640
- /* Ideally the table is to be moved out of here for faster perf.,
641
- but it's quite tricky since it relies on all these `parsers`
642
- and `expect` available only here */
643
- return {
644
- alpha: f(parsers.ieAlpha, true),
645
- boolean: f(condition),
646
- 'if': f(condition)
647
- }[name.toLowerCase()];
648
-
649
- function f(parse, stop) {
650
- return {
651
- parse, // parsing function
652
- stop // when true - stop after parse() and return its result,
653
- // otherwise continue for plain args
654
- };
655
- }
656
-
657
- function condition() {
658
- return [expect(() => parsers.condition(false, true), 'expected condition')];
659
- }
660
- },
661
-
662
- arguments: function (prevArgs) {
663
- let argsComma = prevArgs || [];
664
- const argsSemiColon = [];
665
- let isSemiColonSeparated;
666
- let value;
667
-
668
- parserInput.save();
669
-
670
- while (true) {
671
- if (prevArgs) {
672
- prevArgs = false;
673
- } else {
674
- value = parsers.detachedRuleset() || this.assignment() || parsers.expression();
675
- if (!value) {
676
- break;
677
- }
678
-
679
- if (value.value && value.value.length == 1) {
680
- value = value.value[0];
681
- }
682
-
683
- argsComma.push(value);
684
- }
685
-
686
- if (parserInput.$char(',')) {
687
- continue;
688
- }
689
-
690
- if (parserInput.$char(';') || isSemiColonSeparated) {
691
- isSemiColonSeparated = true;
692
- value = (argsComma.length < 1) ? argsComma[0]
693
- : new tree.Value(argsComma);
694
- argsSemiColon.push(value);
695
- argsComma = [];
696
- }
697
- }
698
-
699
- parserInput.forget();
700
- return isSemiColonSeparated ? argsSemiColon : argsComma;
701
- },
702
- literal: function () {
703
- return this.dimension() ||
704
- this.color() ||
705
- this.quoted() ||
706
- this.unicodeDescriptor();
707
- },
708
-
709
- // Assignments are argument entities for calls.
710
- // They are present in ie filter properties as shown below.
711
- //
712
- // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
713
- //
714
-
715
- assignment: function () {
716
- let key;
717
- let value;
718
- parserInput.save();
719
- key = parserInput.$re(/^\w+(?=\s?=)/i);
720
- if (!key) {
721
- parserInput.restore();
722
- return;
723
- }
724
- if (!parserInput.$char('=')) {
725
- parserInput.restore();
726
- return;
727
- }
728
- value = parsers.entity();
729
- if (value) {
730
- parserInput.forget();
731
- return new(tree.Assignment)(key, value);
732
- } else {
733
- parserInput.restore();
734
- }
735
- },
736
-
737
- //
738
- // Parse url() tokens
739
- //
740
- // We use a specific rule for urls, because they don't really behave like
741
- // standard function calls. The difference is that the argument doesn't have
742
- // to be enclosed within a string, so it can't be parsed as an Expression.
743
- //
744
- url: function () {
745
- let value;
746
- const index = parserInput.i;
747
-
748
- parserInput.autoCommentAbsorb = false;
749
-
750
- if (!parserInput.$str('url(')) {
751
- parserInput.autoCommentAbsorb = true;
752
- return;
753
- }
754
-
755
- value = this.quoted() || this.variable() || this.property() ||
756
- parserInput.$re(/^(?:(?:\\[()'"])|[^()'"])+/) || '';
757
-
758
- // An unquoted url() body is otherwise raw text wrapped in an
759
- // Anonymous node, which never substitutes anything — so
760
- // `url(@{path}/a.png)` used to emit the braces verbatim while the
761
- // quoted form resolved. Hand text containing interpolation to an
762
- // escaped Quoted so both spellings resolve identically.
763
- //
764
- // The empty quote string matters: `URL.eval` escapes a rewritten
765
- // rootpath only for unquoted values, so a real quote character here
766
- // would suppress that and emit `url(a(b)/x.png)` unescaped. Escaped
767
- // means no quote is written to the output either way.
768
- if (typeof value === 'string' && hasInterpolation(value)) {
769
- value = new(tree.Quoted)('', value, true, index + currentIndex, fileInfo);
770
- }
771
-
772
- parserInput.autoCommentAbsorb = true;
773
-
774
- expectChar(')');
775
-
776
- return new(tree.URL)((value.value !== undefined ||
777
- value instanceof tree.Variable ||
778
- value instanceof tree.Property) ?
779
- value : new(tree.Anonymous)(value, index), index + currentIndex, fileInfo);
780
- },
781
-
782
- //
783
- // A Variable entity, such as `@fink`, in
784
- //
785
- // width: @fink + 2px
786
- //
787
- // We use a different parser for variable definitions,
788
- // see `parsers.variable`.
789
- //
790
- variable: function () {
791
- let ch;
792
- let name;
793
- const index = parserInput.i;
794
-
795
- parserInput.save();
796
- if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^@@?[\w-]+/))) {
797
- warnNumericVariableName(name, index);
798
- warnDashOnlyVariableName(name, index);
799
- ch = parserInput.currentChar();
800
- if ((ch === '(' && !parserInput.prevChar().match(/^\s/))
801
- || (ch === '[' && !parserInput.prevChar().match(/^\s/))) {
802
- // this may be a VariableCall lookup
803
- const result = parsers.variableCall(name);
804
- if (result) {
805
- parserInput.forget();
806
- return result;
807
- }
808
- }
809
- parserInput.forget();
810
- return new(tree.Variable)(name, index + currentIndex, fileInfo);
811
- }
812
- parserInput.restore();
813
- },
814
-
815
- // A variable entity using the protective {} e.g. @{var}, optionally
816
- // followed by a lookup chain e.g. @{map[key]} or @{map[@a][b]}.
817
- //
818
- // The chain is consumed by `mixin.ruleLookups()` rather than matched
819
- // here, so the interpolated form shares one grammar with the bare
820
- // `@map[key]` form instead of re-implementing it.
821
- variableCurly: function () {
822
- let curly;
823
- const index = parserInput.i;
824
-
825
- if (parserInput.currentChar() !== '@') {
826
- return;
827
- }
828
-
829
- parserInput.save();
830
- if (!(curly = parserInput.$re(/^@\{([\w-]+)/))) {
831
- parserInput.restore();
832
- return;
833
- }
834
-
835
- const name = curly[1];
836
- const lookups = parsers.mixin.ruleLookups();
837
-
838
- if (!parserInput.$char('}')) {
839
- parserInput.restore();
840
- return;
841
- }
842
-
843
- parserInput.forget();
844
- warnNumericVariableName(name, index);
845
- warnDashOnlyVariableName(name, index);
846
-
847
- if (!lookups) {
848
- return new(tree.Variable)(`@${name}`, index + currentIndex, fileInfo);
849
- }
850
-
851
- const call = new(tree.VariableCall)(`@${name}`, index + currentIndex, fileInfo);
852
- return new(tree.NamespaceValue)(call, lookups, index + currentIndex, fileInfo);
853
- },
854
- //
855
- // A Property accessor, such as `$color`, in
856
- //
857
- // background-color: $color
858
- //
859
- property: function () {
860
- let name;
861
- const index = parserInput.i;
862
-
863
- if (parserInput.currentChar() === '$' && (name = parserInput.$re(/^\$[\w-]+/))) {
864
- return new(tree.Property)(name, index + currentIndex, fileInfo);
865
- }
866
- },
867
-
868
- //
869
- // A Hexadecimal color
870
- //
871
- // #4F3C2F
872
- //
873
- // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
874
- //
875
- color: function () {
876
- let rgb;
877
- parserInput.save();
878
-
879
- if (parserInput.currentChar() === '#' && (rgb = parserInput.$re(/^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3,4})([\w.#[])?/))) {
880
- if (!rgb[2]) {
881
- parserInput.forget();
882
- return new(tree.Color)(rgb[1], undefined, rgb[0]);
883
- }
884
- }
885
- parserInput.restore();
886
- },
887
-
888
- colorKeyword: function () {
889
- parserInput.save();
890
- const autoCommentAbsorb = parserInput.autoCommentAbsorb;
891
- parserInput.autoCommentAbsorb = false;
892
- const k = parserInput.$re(/^[_A-Za-z-][_A-Za-z0-9-]+/);
893
- parserInput.autoCommentAbsorb = autoCommentAbsorb;
894
- if (!k) {
895
- parserInput.forget();
896
- return;
897
- }
898
- parserInput.restore();
899
- const color = tree.Color.fromKeyword(k);
900
- if (color) {
901
- parserInput.$str(k);
902
- return color;
903
- }
904
- },
905
-
906
- //
907
- // A Dimension, that is, a number and a unit
908
- //
909
- // 0.5em 95%
910
- //
911
- dimension: function () {
912
- if (parserInput.peekNotNumeric()) {
913
- return;
914
- }
915
-
916
- const value = parserInput.$re(/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/i);
917
- if (value) {
918
- return new(tree.Dimension)(value[1], value[2]);
919
- }
920
- },
921
-
922
- //
923
- // A unicode descriptor, as is used in unicode-range
924
- //
925
- // U+0?? or U+00A1-00A9
926
- //
927
- unicodeDescriptor: function () {
928
- let ud;
929
-
930
- ud = parserInput.$re(/^U\+[0-9a-fA-F?]+(-[0-9a-fA-F?]+)?/);
931
- if (ud) {
932
- return new(tree.UnicodeDescriptor)(ud[0]);
933
- }
934
- },
935
-
936
- //
937
- // JavaScript code to be evaluated
938
- //
939
- // `window.location.href`
940
- //
941
- javascript: function () {
942
- let js;
943
- const index = parserInput.i;
944
-
945
- parserInput.save();
946
-
947
- const escape = parserInput.$char('~');
948
- const jsQuote = parserInput.$char('`');
949
-
950
- if (!jsQuote) {
951
- parserInput.restore();
952
- return;
953
- }
954
-
955
- js = parserInput.$re(/^[^`]*`/);
956
- if (js) {
957
- warn('Inline JavaScript evaluation (backtick expressions) is deprecated and will be removed in Less 5.x. Use Less functions or custom plugins instead.', index, 'DEPRECATED', 'js-eval');
958
- parserInput.forget();
959
- return new(tree.JavaScript)(js.slice(0, -1), Boolean(escape), index + currentIndex, fileInfo);
960
- }
961
- parserInput.restore('invalid javascript definition');
962
- }
963
- },
964
-
965
- //
966
- // The variable part of a variable definition. Used in the `rule` parser
967
- //
968
- // @fink:
969
- //
970
- variable: function () {
971
- let name;
972
-
973
- if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^(@[\w-]+)\s*:/))) {
974
- warnNumericVariableName(name[1], parserInput.i - name[0].length);
975
- warnDashOnlyVariableName(name[1], parserInput.i - name[0].length);
976
- return name[1];
977
- }
978
- },
979
-
980
- //
981
- // Call a variable value to retrieve a detached ruleset
982
- // or a value from a detached ruleset's rules.
983
- //
984
- // @fink();
985
- // @fink;
986
- // color: @fink[@color];
987
- //
988
- variableCall: function (parsedName) {
989
- let lookups;
990
- const i = parserInput.i;
991
- const inValue = !!parsedName;
992
- let name = parsedName;
993
-
994
- parserInput.save();
995
-
996
- if (name || (parserInput.currentChar() === '@'
997
- && (name = parserInput.$re(/^(@[\w-]+)(\(\s*\))?/)))) {
998
-
999
- lookups = this.mixin.ruleLookups();
1000
-
1001
- if (!lookups && ((inValue && parserInput.$str('()') !== '()') || (name[2] !== '()'))) {
1002
- parserInput.restore('Missing \'[...]\' lookup in variable call');
1003
- return;
1004
- }
1005
-
1006
- if (!inValue) {
1007
- warnNumericVariableName(name[1], i);
1008
- warnDashOnlyVariableName(name[1], i);
1009
- name = name[1];
1010
- }
1011
-
1012
- const call = new tree.VariableCall(name, i, fileInfo);
1013
- if (!inValue && parsers.end()) {
1014
- parserInput.forget();
1015
- return call;
1016
- }
1017
- else {
1018
- parserInput.forget();
1019
- return new tree.NamespaceValue(call, lookups, i, fileInfo);
1020
- }
1021
- }
1022
-
1023
- parserInput.restore();
1024
- },
1025
-
1026
- //
1027
- // extend syntax - used to extend selectors
1028
- //
1029
- extend: function(isRule) {
1030
- let elements;
1031
- let e;
1032
- const index = parserInput.i;
1033
- let option;
1034
- let extendList;
1035
- let extend;
1036
-
1037
- if (!parserInput.$str(isRule ? '&:extend(' : ':extend(')) {
1038
- return;
1039
- }
1040
-
1041
- do {
1042
- option = null;
1043
- elements = null;
1044
- let first = true;
1045
- while (!(option = parserInput.$re(/^(!?all)(?=\s*(\)|,))/))) {
1046
- e = this.element();
1047
-
1048
- if (!e) {
1049
- break;
1050
- }
1051
- /**
1052
- * @note - This will not catch selectors in pseudos like :is() and :where() because
1053
- * they don't currently parse their contents as selectors.
1054
- */
1055
- if (!first && e.combinator.value) {
1056
- warn('Targeting complex selectors can have unexpected behavior, and this behavior may change in the future.', index)
1057
- }
1058
-
1059
- first = false;
1060
- if (elements) {
1061
- elements.push(e);
1062
- } else {
1063
- elements = [ e ];
1064
- }
1065
- }
1066
-
1067
- option = option && option[1];
1068
- if (!elements) {
1069
- error('Missing target selector for :extend().');
1070
- }
1071
- extend = new(tree.Extend)(new(tree.Selector)(elements), option, index + currentIndex, fileInfo);
1072
- if (extendList) {
1073
- extendList.push(extend);
1074
- } else {
1075
- extendList = [ extend ];
1076
- }
1077
- } while (parserInput.$char(','));
1078
-
1079
- expect(/^\)/);
1080
-
1081
- if (isRule) {
1082
- expect(/^;/);
1083
- }
1084
-
1085
- return extendList;
1086
- },
1087
-
1088
- //
1089
- // extendRule - used in a rule to extend all the parent selectors
1090
- //
1091
- extendRule: function() {
1092
- return this.extend(true);
1093
- },
1094
-
1095
- //
1096
- // Mixins
1097
- //
1098
- mixin: {
1099
- //
1100
- // A Mixin call, with an optional argument list
1101
- //
1102
- // #mixins > .square(#fff);
1103
- // #mixins.square(#fff);
1104
- // .rounded(4px, black);
1105
- // .button;
1106
- //
1107
- // We can lookup / return a value using the lookup syntax:
1108
- //
1109
- // color: #mixin.square(#fff)[@color];
1110
- //
1111
- // The `while` loop is there because mixins can be
1112
- // namespaced, but we only support the child and descendant
1113
- // selector for now.
1114
- //
1115
- call: function (inValue, getLookup) {
1116
- const s = parserInput.currentChar();
1117
- let important = false;
1118
- let lookups;
1119
- const index = parserInput.i;
1120
- let elements;
1121
- let args;
1122
- let hasParens;
1123
- let parensIndex;
1124
- let parensWS = false;
1125
-
1126
- if (s !== '.' && s !== '#') { return; }
1127
-
1128
- parserInput.save(); // stop us absorbing part of an invalid selector
1129
-
1130
- elements = this.elements();
1131
-
1132
- if (elements) {
1133
- parensIndex = parserInput.i;
1134
- parensWS = parserInput.isWhitespace(-1);
1135
- if (parserInput.$char('(')) {
1136
- args = this.args(true).args;
1137
- expectChar(')');
1138
- hasParens = true;
1139
- if (parensWS) {
1140
- warn('Whitespace between a mixin name and parentheses for a mixin call is deprecated', parensIndex, 'DEPRECATED', 'mixin-call-whitespace');
1141
- }
1142
- }
1143
-
1144
- if (getLookup !== false) {
1145
- lookups = this.ruleLookups();
1146
- }
1147
- if (getLookup === true && !lookups) {
1148
- parserInput.restore();
1149
- return;
1150
- }
1151
-
1152
- if (inValue && !lookups && !hasParens) {
1153
- // This isn't a valid in-value mixin call
1154
- parserInput.restore();
1155
- return;
1156
- }
1157
-
1158
- if (!inValue && parsers.important()) {
1159
- important = true;
1160
- }
1161
-
1162
- if (inValue || parsers.end()) {
1163
- parserInput.forget();
1164
- const mixin = new(tree.mixin.Call)(elements, args, index + currentIndex, fileInfo, !lookups && important);
1165
- for (const element of elements) {
1166
- warnDashOnlyMixinName(element.value, element._index - currentIndex);
1167
- }
1168
- if (lookups) {
1169
- return new tree.NamespaceValue(mixin, lookups);
1170
- }
1171
- else {
1172
- if (!hasParens) {
1173
- warn('Calling a mixin without parentheses is deprecated', parensIndex, 'DEPRECATED', 'mixin-call-no-parens');
1174
- }
1175
- return mixin;
1176
- }
1177
- }
1178
- }
1179
-
1180
- parserInput.restore();
1181
- },
1182
- /**
1183
- * Matching elements for mixins
1184
- * (Start with . or # and can have > )
1185
- */
1186
- elements: function() {
1187
- let elements;
1188
- let e;
1189
- let c;
1190
- let elem;
1191
- let elemIndex;
1192
- const re = /^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/;
1193
- while (true) {
1194
- elemIndex = parserInput.i;
1195
- e = parserInput.$re(re);
1196
-
1197
- if (!e) {
1198
- break;
1199
- }
1200
- elem = new(tree.Element)(c, e, false, elemIndex + currentIndex, fileInfo);
1201
- if (elements) {
1202
- elements.push(elem);
1203
- } else {
1204
- elements = [ elem ];
1205
- }
1206
- c = parserInput.$char('>');
1207
- }
1208
- return elements;
1209
- },
1210
- args: function (isCall) {
1211
- const entities = parsers.entities;
1212
- const returner = { args:null, variadic: false };
1213
- let expressions = [];
1214
- const argsSemiColon = [];
1215
- const argsComma = [];
1216
- let isSemiColonSeparated;
1217
- let expressionContainsNamed;
1218
- let name;
1219
- let nameLoop;
1220
- let value;
1221
- let arg;
1222
- let expand;
1223
- let hasSep = true;
1224
-
1225
- parserInput.save();
1226
-
1227
- while (true) {
1228
- if (isCall) {
1229
- arg = parsers.detachedRuleset() || parsers.expression();
1230
- } else {
1231
- parserInput.commentStore.length = 0;
1232
- if (parserInput.$str('...')) {
1233
- returner.variadic = true;
1234
- if (parserInput.$char(';') && !isSemiColonSeparated) {
1235
- isSemiColonSeparated = true;
1236
- }
1237
- (isSemiColonSeparated ? argsSemiColon : argsComma)
1238
- .push({ variadic: true });
1239
- break;
1240
- }
1241
- arg = entities.variable() || entities.property() || entities.literal() || entities.keyword() || this.call(true);
1242
- }
1243
-
1244
- if (!arg || !hasSep) {
1245
- break;
1246
- }
1247
-
1248
- nameLoop = null;
1249
- if (arg.throwAwayComments) {
1250
- arg.throwAwayComments();
1251
- }
1252
- value = arg;
1253
- let val = null;
1254
-
1255
- if (isCall) {
1256
- // Variable
1257
- if (arg.value && arg.value.length == 1) {
1258
- val = arg.value[0];
1259
- }
1260
- } else {
1261
- val = arg;
1262
- }
1263
-
1264
- if (val && (val instanceof tree.Variable || val instanceof tree.Property)) {
1265
- if (parserInput.$char(':')) {
1266
- if (expressions.length > 0) {
1267
- if (isSemiColonSeparated) {
1268
- error('Cannot mix ; and , as delimiter types');
1269
- }
1270
- expressionContainsNamed = true;
1271
- }
1272
-
1273
- value = parsers.detachedRuleset() || parsers.expression();
1274
-
1275
- if (!value) {
1276
- if (isCall) {
1277
- error('could not understand value for named argument');
1278
- } else {
1279
- parserInput.restore();
1280
- returner.args = [];
1281
- return returner;
1282
- }
1283
- }
1284
- nameLoop = (name = val.name);
1285
- } else if (parserInput.$str('...')) {
1286
- if (!isCall) {
1287
- returner.variadic = true;
1288
- if (parserInput.$char(';') && !isSemiColonSeparated) {
1289
- isSemiColonSeparated = true;
1290
- }
1291
- (isSemiColonSeparated ? argsSemiColon : argsComma)
1292
- .push({ name: arg.name, variadic: true });
1293
- break;
1294
- } else {
1295
- expand = true;
1296
- }
1297
- } else if (!isCall) {
1298
- name = nameLoop = val.name;
1299
- value = null;
1300
- }
1301
- }
1302
-
1303
- if (value) {
1304
- expressions.push(value);
1305
- }
1306
-
1307
- argsComma.push({ name:nameLoop, value, expand });
1308
-
1309
- if (parserInput.$char(',')) {
1310
- hasSep = true;
1311
- continue;
1312
- }
1313
- hasSep = parserInput.$char(';') === ';';
1314
-
1315
- if (hasSep || isSemiColonSeparated) {
1316
-
1317
- if (expressionContainsNamed) {
1318
- error('Cannot mix ; and , as delimiter types');
1319
- }
1320
-
1321
- isSemiColonSeparated = true;
1322
-
1323
- if (expressions.length > 1) {
1324
- value = new(tree.Value)(expressions);
1325
- }
1326
- argsSemiColon.push({ name, value, expand });
1327
-
1328
- name = null;
1329
- expressions = [];
1330
- expressionContainsNamed = false;
1331
- }
1332
- }
1333
-
1334
- parserInput.forget();
1335
- returner.args = isSemiColonSeparated ? argsSemiColon : argsComma;
1336
- return returner;
1337
- },
1338
- //
1339
- // A Mixin definition, with a list of parameters
1340
- //
1341
- // .rounded (@radius: 2px, @color) {
1342
- // ...
1343
- // }
1344
- //
1345
- // Until we have a finer grained state-machine, we have to
1346
- // do a look-ahead, to make sure we don't have a mixin call.
1347
- // See the `rule` function for more information.
1348
- //
1349
- // We start by matching `.rounded (`, and then proceed on to
1350
- // the argument list, which has optional default values.
1351
- // We store the parameters in `params`, with a `value` key,
1352
- // if there is a value, such as in the case of `@radius`.
1353
- //
1354
- // Once we've got our params list, and a closing `)`, we parse
1355
- // the `{...}` block.
1356
- //
1357
- definition: function () {
1358
- let name;
1359
- let params = [];
1360
- let match;
1361
- let ruleset;
1362
- let cond;
1363
- let variadic = false;
1364
- const index = parserInput.i;
1365
- if ((parserInput.currentChar() !== '.' && parserInput.currentChar() !== '#') ||
1366
- parserInput.peek(/^[^{]*\}/)) {
1367
- return;
1368
- }
1369
-
1370
- parserInput.save();
1371
-
1372
- match = parserInput.$re(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/);
1373
- if (match) {
1374
- name = match[1];
1375
-
1376
- const argInfo = this.args(false);
1377
- params = argInfo.args;
1378
- variadic = argInfo.variadic;
1379
-
1380
- // .mixincall("@{a}");
1381
- // looks a bit like a mixin definition..
1382
- // also
1383
- // .mixincall(@a: {rule: set;});
1384
- // so we have to be nice and restore
1385
- if (!parserInput.$char(')')) {
1386
- parserInput.restore('Missing closing \')\'');
1387
- return;
1388
- }
1389
-
1390
- parserInput.commentStore.length = 0;
1391
-
1392
- if (parserInput.$str('when')) { // Guard
1393
- cond = expect(parsers.conditions, 'expected condition');
1394
- }
1395
-
1396
- ruleset = parsers.block();
1397
-
1398
- if (ruleset) {
1399
- parserInput.forget();
1400
- warnDashOnlyMixinName(name, index);
1401
- return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);
1402
- } else {
1403
- parserInput.restore();
1404
- }
1405
- } else {
1406
- parserInput.restore();
1407
- }
1408
- },
1409
-
1410
- ruleLookups: function() {
1411
- let rule;
1412
- const lookups = [];
1413
-
1414
- if (parserInput.currentChar() !== '[') {
1415
- return;
1416
- }
1417
-
1418
- while (true) {
1419
- parserInput.save();
1420
- rule = this.lookupValue();
1421
- if (!rule && rule !== '') {
1422
- parserInput.restore();
1423
- break;
1424
- }
1425
- lookups.push(rule);
1426
- parserInput.forget();
1427
- }
1428
- if (lookups.length > 0) {
1429
- return lookups;
1430
- }
1431
- },
1432
-
1433
- lookupValue: function() {
1434
- parserInput.save();
1435
-
1436
- if (!parserInput.$char('[')) {
1437
- parserInput.restore();
1438
- return;
1439
- }
1440
-
1441
- const name = parserInput.$re(/^(?:[@$]{0,2})[_a-zA-Z0-9-]*/);
1442
-
1443
- if (!parserInput.$char(']')) {
1444
- parserInput.restore();
1445
- return;
1446
- }
1447
-
1448
- if (name || name === '') {
1449
- parserInput.forget();
1450
- return name;
1451
- }
1452
-
1453
- parserInput.restore();
1454
- }
1455
- },
1456
- //
1457
- // Entities are the smallest recognized token,
1458
- // and can be found inside a rule's value.
1459
- //
1460
- entity: function () {
1461
- const entities = this.entities;
1462
-
1463
- return this.comment() || entities.literal() || entities.variable() || entities.url() ||
1464
- entities.property() || entities.call() || entities.keyword() || this.mixin.call(true) ||
1465
- entities.javascript();
1466
- },
1467
-
1468
- //
1469
- // A Declaration terminator. Note that we use `peek()` to check for '}',
1470
- // because the `block` rule will be expecting it, but we still need to make sure
1471
- // it's there, if ';' was omitted.
1472
- //
1473
- end: function () {
1474
- return parserInput.$char(';') || parserInput.peek('}');
1475
- },
1476
-
1477
- //
1478
- // IE's alpha function
1479
- //
1480
- // alpha(opacity=88)
1481
- //
1482
- ieAlpha: function () {
1483
- let value;
1484
-
1485
- // http://jsperf.com/case-insensitive-regex-vs-strtolower-then-regex/18
1486
- if (!parserInput.$re(/^opacity=/i)) { return; }
1487
- value = parserInput.$re(/^\d+/);
1488
- if (!value) {
1489
- value = expect(parsers.entities.variable, 'Could not parse alpha');
1490
- value = `@{${value.name.slice(1)}}`;
1491
- }
1492
- expectChar(')');
1493
- return new tree.Quoted('', `alpha(opacity=${value})`);
1494
- },
1495
-
1496
- /**
1497
- * A Selector Element
1498
- *
1499
- * div
1500
- * + h1
1501
- * #socks
1502
- * input[type="text"]
1503
- *
1504
- * Elements are the building blocks for Selectors,
1505
- * they are made out of a `Combinator` (see combinator rule),
1506
- * and an element name, such as a tag a class, or `*`.
1507
- */
1508
- element: function () {
1509
- let e;
1510
- let c;
1511
- let v;
1512
- const index = parserInput.i;
1513
-
1514
- c = this.combinator();
1515
-
1516
- /** This selector parser is quite simplistic and will pass a number of invalid selectors. */
1517
- e = parserInput.$re(/^(?:\d+\.\d+|\d+)%/) ||
1518
- // eslint-disable-next-line no-control-regex
1519
- parserInput.$re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||
1520
- parserInput.$char('*') || parserInput.$char('&') || this.attribute() ||
1521
- parserInput.$re(/^\([^&()@]+\)/) || parserInput.$re(/^[.#:](?=@)/) ||
1522
- this.entities.variableCurly();
1523
-
1524
- if (!e) {
1525
- parserInput.save();
1526
- if (parserInput.$char('(')) {
1527
- if ((v = this.selector(false))) {
1528
- let selectors = [];
1529
- while (parserInput.$char(',')) {
1530
- selectors.push(v);
1531
- selectors.push(new Anonymous(','));
1532
- v = this.selector(false);
1533
- }
1534
- selectors.push(v);
1535
-
1536
- if (parserInput.$char(')')) {
1537
- if (selectors.length > 1) {
1538
- e = new (tree.Paren)(new Selector(selectors));
1539
- } else {
1540
- e = new(tree.Paren)(v);
1541
- }
1542
- parserInput.forget();
1543
- } else {
1544
- parserInput.restore('Missing closing \')\'');
1545
- }
1546
- } else {
1547
- parserInput.restore('Missing closing \')\'');
1548
- }
1549
- } else {
1550
- parserInput.forget();
1551
- }
1552
- }
1553
-
1554
- if (e) { return new(tree.Element)(c, e, e instanceof tree.Variable, index + currentIndex, fileInfo); }
1555
- },
1556
-
1557
- //
1558
- // Combinators combine elements together, in a Selector.
1559
- //
1560
- // Because our parser isn't white-space sensitive, special care
1561
- // has to be taken, when parsing the descendant combinator, ` `,
1562
- // as it's an empty space. We have to check the previous character
1563
- // in the input, to see if it's a ` ` character. More info on how
1564
- // we deal with this in *combinator.js*.
1565
- //
1566
- combinator: function () {
1567
- let c = parserInput.currentChar();
1568
-
1569
- if (c === '/') {
1570
- parserInput.save();
1571
- const slashedCombinator = parserInput.$re(/^\/[a-z]+\//i);
1572
- if (slashedCombinator) {
1573
- parserInput.forget();
1574
- return new(tree.Combinator)(slashedCombinator);
1575
- }
1576
- parserInput.restore();
1577
- }
1578
-
1579
- if (c === '>' || c === '+' || c === '~' || c === '|' || c === '^') {
1580
- parserInput.i++;
1581
- if (c === '^' && parserInput.currentChar() === '^') {
1582
- c = '^^';
1583
- parserInput.i++;
1584
- }
1585
- while (parserInput.isWhitespace()) { parserInput.i++; }
1586
- return new(tree.Combinator)(c);
1587
- } else if (parserInput.isWhitespace(-1)) {
1588
- return new(tree.Combinator)(' ');
1589
- } else {
1590
- return new(tree.Combinator)(null);
1591
- }
1592
- },
1593
- //
1594
- // A CSS Selector
1595
- // with less extensions e.g. the ability to extend and guard
1596
- //
1597
- // .class > div + h1
1598
- // li a:hover
1599
- //
1600
- // Selectors are made out of one or more Elements, see above.
1601
- //
1602
- selector: function (isLess) {
1603
- const index = parserInput.i;
1604
- let elements;
1605
- let extendList;
1606
- let c;
1607
- let e;
1608
- let allExtends;
1609
- let when;
1610
- let condition;
1611
- isLess = isLess !== false;
1612
- while ((isLess && (extendList = this.extend())) || (isLess && (when = parserInput.$str('when'))) || (e = this.element())) {
1613
- if (when) {
1614
- condition = expect(this.conditions, 'expected condition');
1615
- } else if (condition) {
1616
- error('CSS guard can only be used at the end of selector');
1617
- } else if (extendList) {
1618
- if (allExtends) {
1619
- allExtends = allExtends.concat(extendList);
1620
- } else {
1621
- allExtends = extendList;
1622
- }
1623
- } else {
1624
- if (allExtends) { error('Extend can only be used at the end of selector'); }
1625
- c = parserInput.currentChar();
1626
- if (Array.isArray(e)){
1627
- e.forEach(ele => elements.push(ele));
1628
- } if (elements) {
1629
- elements.push(e);
1630
- } else {
1631
- elements = [ e ];
1632
- }
1633
- e = null;
1634
- }
1635
- if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') {
1636
- break;
1637
- }
1638
- }
1639
-
1640
- if (elements) { return new(tree.Selector)(elements, allExtends, condition, index + currentIndex, fileInfo); }
1641
- if (allExtends) { error('Extend must be used to extend a selector, it cannot be used on its own'); }
1642
- },
1643
- selectors: function () {
1644
- let s;
1645
- let selectors;
1646
- while (true) {
1647
- s = this.selector();
1648
- if (!s) {
1649
- break;
1650
- }
1651
- if (selectors) {
1652
- selectors.push(s);
1653
- } else {
1654
- selectors = [ s ];
1655
- }
1656
- parserInput.commentStore.length = 0;
1657
- if (s.condition && selectors.length > 1) {
1658
- error('Guards are only currently allowed on a single selector.');
1659
- }
1660
- if (!parserInput.$char(',')) { break; }
1661
- if (s.condition) {
1662
- error('Guards are only currently allowed on a single selector.');
1663
- }
1664
- parserInput.commentStore.length = 0;
1665
- }
1666
- return selectors;
1667
- },
1668
- attribute: function () {
1669
- if (!parserInput.$char('[')) { return; }
1670
-
1671
- const entities = this.entities;
1672
- let key;
1673
- let val;
1674
- let op;
1675
- //
1676
- // case-insensitive flag
1677
- // e.g. [attr operator value i]
1678
- //
1679
- let cif;
1680
-
1681
- if (!(key = entities.variableCurly())) {
1682
- key = expect(/^(?:[_A-Za-z0-9-*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/);
1683
- }
1684
-
1685
- op = parserInput.$re(/^[|~*$^]?=/);
1686
- if (op) {
1687
- val = entities.quoted() || parserInput.$re(/^[0-9]+%/) || parserInput.$re(/^[\w-]+/) || entities.variableCurly();
1688
- if (val) {
1689
- cif = parserInput.$re(/^[iIsS]/);
1690
- }
1691
- }
1692
-
1693
- expectChar(']');
1694
-
1695
- return new(tree.Attribute)(key, op, val, cif);
1696
- },
1697
-
1698
- //
1699
- // The `block` rule is used by `ruleset` and `mixin.definition`.
1700
- // It's a wrapper around the `primary` rule, with added `{}`.
1701
- //
1702
- block: function () {
1703
- let content;
1704
- if (parserInput.$char('{') && (content = this.primary()) && parserInput.$char('}')) {
1705
- return content;
1706
- }
1707
- },
1708
-
1709
- blockRuleset: function() {
1710
- let block = this.block();
1711
- if (block) {
1712
- return new tree.Ruleset(null, block);
1713
- }
1714
- },
1715
-
1716
- detachedRuleset: function() {
1717
- let argInfo;
1718
- let params;
1719
- let variadic;
1720
-
1721
- parserInput.save();
1722
- if (parserInput.$re(/^[.#]\(/)) {
1723
- /**
1724
- * DR args currently only implemented for each() function, and not
1725
- * yet settable as `@dr: #(@arg) {}`
1726
- * This should be done when DRs are merged with mixins.
1727
- * See: https://github.com/less/less-meta/issues/16
1728
- */
1729
- argInfo = this.mixin.args(false);
1730
- params = argInfo.args;
1731
- variadic = argInfo.variadic;
1732
- if (!parserInput.$char(')')) {
1733
- parserInput.restore();
1734
- return;
1735
- }
1736
- }
1737
- const blockRuleset = this.blockRuleset();
1738
- if (blockRuleset) {
1739
- parserInput.forget();
1740
- if (params) {
1741
- return new tree.mixin.Definition(null, params, blockRuleset, null, variadic);
1742
- }
1743
- return new tree.DetachedRuleset(blockRuleset);
1744
- }
1745
- parserInput.restore();
1746
- },
1747
-
1748
- //
1749
- // div, .class, body > p {...}
1750
- //
1751
- ruleset: function () {
1752
- let selectors;
1753
- let rules;
1754
- let debugInfo;
1755
-
1756
- parserInput.save();
1757
-
1758
- if (context.dumpLineNumbers) {
1759
- debugInfo = getDebugInfo(parserInput.i);
1760
- }
1761
-
1762
- selectors = this.selectors();
1763
-
1764
- if (selectors && (rules = this.block())) {
1765
- parserInput.forget();
1766
- const ruleset = new(tree.Ruleset)(selectors, rules, context.strictImports);
1767
- if (context.dumpLineNumbers) {
1768
- ruleset.debugInfo = debugInfo;
1769
- }
1770
- return ruleset;
1771
- } else {
1772
- parserInput.restore();
1773
- }
1774
- },
1775
- declaration: function () {
1776
- let name;
1777
- let value;
1778
- const index = parserInput.i;
1779
- let hasDR;
1780
- const c = parserInput.currentChar();
1781
- let important;
1782
- let merge;
1783
- let isVariable;
1784
-
1785
- if (c === '.' || c === '#' || c === '&' || c === ':') { return; }
1786
-
1787
- parserInput.save();
1788
-
1789
- name = this.variable() || this.ruleProperty();
1790
- if (name) {
1791
- isVariable = typeof name === 'string';
1792
-
1793
- if (isVariable) {
1794
- value = this.detachedRuleset();
1795
- if (value) {
1796
- hasDR = true;
1797
- }
1798
- }
1799
-
1800
- parserInput.commentStore.length = 0;
1801
- if (!value) {
1802
- // a name returned by this.ruleProperty() is always an array of the form:
1803
- // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
1804
- // where each item is a tree.Keyword or tree.Variable
1805
- merge = !isVariable && name.length > 1 && name.pop().value;
1806
-
1807
- // Custom property values get permissive parsing
1808
- // A lookup particle (`NamespaceValue`) carries a node in `value`
1809
- // rather than a string, and can never spell a `--` prefix.
1810
- if (typeof name[0].value === 'string' && name[0].value.slice(0, 2) === '--') {
1811
- if (parserInput.$char(';')) {
1812
- value = new Anonymous('');
1813
- } else {
1814
- value = this.permissiveValue(/[;}]/);
1815
- }
1816
- }
1817
- // Try to store values as anonymous
1818
- // If we need the value later we'll re-parse it in ruleset.parseValue
1819
- else {
1820
- value = this.anonymousValue();
1821
- }
1822
- if (value) {
1823
- parserInput.forget();
1824
- // anonymous values absorb the end ';' which is required for them to work
1825
- return new(tree.Declaration)(name, value, false, merge, index + currentIndex, fileInfo);
1826
- }
1827
-
1828
- if (!value) {
1829
- value = this.value();
1830
- }
1831
-
1832
- if (value) {
1833
- important = this.important();
1834
- } else if (isVariable) {
1835
- /**
1836
- * As a last resort, try permissiveValue
1837
- *
1838
- * @todo - This has created some knock-on problems of not
1839
- * flagging incorrect syntax or detecting user intent.
1840
- */
1841
- value = this.permissiveValue();
1842
- }
1843
- }
1844
-
1845
- if (value && (this.end() || hasDR)) {
1846
- parserInput.forget();
1847
- return new(tree.Declaration)(name, value, important, merge, index + currentIndex, fileInfo);
1848
- }
1849
- else {
1850
- parserInput.restore();
1851
- }
1852
- } else {
1853
- parserInput.restore();
1854
- }
1855
- },
1856
- anonymousValue: function () {
1857
- const index = parserInput.i;
1858
- const match = parserInput.$re(/^([^.#@$+/'"*`(;{}-]*);/);
1859
- if (match) {
1860
- return new(tree.Anonymous)(match[1], index + currentIndex);
1861
- }
1862
- },
1863
- /**
1864
- * Used for custom properties, at-rules, and variables (as fallback)
1865
- * Parses almost anything inside of {} [] () "" blocks
1866
- * until it reaches outer-most tokens.
1867
- *
1868
- * First, it will try to parse comments and entities to reach
1869
- * the end. This is mostly like the Expression parser except no
1870
- * math is allowed.
1871
- *
1872
- * @param {RexExp} untilTokens - Characters to stop parsing at
1873
- * @param {boolean} [deprecateVariables] - when set, this is an at-rule
1874
- * prelude (non-value position); accept `@{var}` interpolation and warn
1875
- * on a bare `@var` reference (which resolves today but is deprecated).
1876
- */
1877
- permissiveValue: function (untilTokens, deprecateVariables) {
1878
- const entities = this.entities;
1879
- let i;
1880
- let e;
1881
- let done;
1882
- let value;
1883
- const tok = untilTokens || ';';
1884
- const index = parserInput.i;
1885
- const result = [];
1886
-
1887
- function testCurrentChar() {
1888
- const char = parserInput.currentChar();
1889
- if (typeof tok === 'string') {
1890
- return char === tok;
1891
- } else {
1892
- return tok.test(char);
1893
- }
1894
- }
1895
- if (testCurrentChar()) {
1896
- return;
1897
- }
1898
- value = [];
1899
- do {
1900
- e = this.comment();
1901
- if (e) {
1902
- value.push(e);
1903
- continue;
1904
- }
1905
- if (deprecateVariables) {
1906
- // In an at-rule prelude, `@{var}` interpolation is the supported
1907
- // form; consume it here so its `{` is not mistaken for a block.
1908
- e = entities.variableCurly();
1909
- if (!e) {
1910
- const varIndex = parserInput.i;
1911
- e = this.entity();
1912
- if (isBareVariableReference(e)) {
1913
- warnBareAtRuleVariable(varIndex);
1914
- }
1915
- }
1916
- } else {
1917
- e = this.entity();
1918
- }
1919
- if (e) {
1920
- value.push(e);
1921
- }
1922
- if (parserInput.peek(',')) {
1923
- value.push(new (tree.Anonymous)(',', parserInput.i));
1924
- parserInput.$char(',');
1925
- }
1926
- } while (e);
1927
-
1928
- done = testCurrentChar();
1929
-
1930
- if (value.length > 0) {
1931
- value = new(tree.Expression)(value);
1932
- if (done) {
1933
- return value;
1934
- }
1935
- else {
1936
- result.push(value);
1937
- }
1938
- // Preserve space before $parseUntil as it will not
1939
- if (parserInput.prevChar() === ' ') {
1940
- result.push(new tree.Anonymous(' ', index));
1941
- }
1942
- }
1943
- parserInput.save();
1944
-
1945
- value = parserInput.$parseUntil(tok, deprecateVariables);
1946
-
1947
- if (value) {
1948
- if (typeof value === 'string') {
1949
- error(`Expected '${value}'`, 'Parse');
1950
- }
1951
- if (value.length === 1 && value[0] === ' ') {
1952
- parserInput.forget();
1953
- return new tree.Anonymous('', index);
1954
- }
1955
- // At-rule prelude: `$parseUntil` (deprecateVariables) records the
1956
- // first bare `@var` it saw outside any `(...)` in its single pass —
1957
- // a structural reference (`[...]`/`{...}` don't shield it, only a
1958
- // declaration-value `(...)` does). Warn once here rather than
1959
- // re-scanning the text.
1960
- if (deprecateVariables && value.bareVarIndex !== null && value.bareVarIndex !== undefined) {
1961
- warnBareAtRuleVariable(value.bareVarIndex);
1962
- }
1963
- /** @type {string} */
1964
- let item;
1965
- for (i = 0; i < value.length; i++) {
1966
- item = value[i];
1967
- if (Array.isArray(item)) {
1968
- // Treat actual quotes as normal quoted values
1969
- result.push(new tree.Quoted(item[0], item[1], true, index, fileInfo));
1970
- }
1971
- else {
1972
- if (i === value.length - 1) {
1973
- item = item.trim();
1974
- }
1975
- // Treat like quoted values, but replace vars like unquoted expressions
1976
- const quote = new tree.Quoted('\'', item, true, index, fileInfo);
1977
- const variableRegex = /@([\w-]+)/g;
1978
- const propRegex = /\$([\w-]+)/g;
1979
- // These notices are about *bare* references, so test the text
1980
- // with interpolations removed. A lookup key is itself allowed
1981
- // to be a variable (`@{map[@key]}`), and without this the
1982
- // `@key` inside the braces would be misreported as a bare use
1983
- // of the very syntax the notice tells you to adopt.
1984
- const bareOnly = item
1985
- .replace(VARIABLE_INTERPOLATION, '')
1986
- .replace(PROPERTY_INTERPOLATION, '');
1987
- // At-rule preludes are handled once above via
1988
- // `value.bareVarIndex`; the `variable-in-unknown-value`
1989
- // notice is for unknown declaration values only.
1990
- if (!deprecateVariables && variableRegex.test(bareOnly)) {
1991
- warn('@variable in unknown values will not be evaluated as variables in the future. Use @{variable}', index, 'DEPRECATED', 'variable-in-unknown-value');
1992
- }
1993
- if (propRegex.test(bareOnly)) {
1994
- warn('$property in unknown values will not be evaluated as property references in the future. Use ${property}', index, 'DEPRECATED', 'property-in-unknown-value');
1995
- }
1996
- // Both alternatives carry the lookup chain. An unknown at-rule
1997
- // prelude (`@supports`) is scanned as text, so a bare
1998
- // `@map[key]` reaches this regex rather than being parsed
1999
- // structurally; matching only `@map` would resolve it to the
2000
- // whole ruleset and leave `[key]` behind as literal text.
2001
- quote.variableRegex = new RegExp(
2002
- `@(${VARIABLE_WITH_LOOKUPS})|@\\{(${VARIABLE_WITH_LOOKUPS})\\}`, 'g'
2003
- );
2004
- // Properties stay narrow — they have no lookup grammar.
2005
- quote.propRegex = /\$([\w-]+)|\$\{([\w-]+)\}/g;
2006
- result.push(quote);
2007
- }
2008
- }
2009
- parserInput.forget();
2010
- return new tree.Expression(result, true);
2011
- }
2012
- parserInput.restore();
2013
- },
2014
-
2015
- //
2016
- // An @import atrule
2017
- //
2018
- // @import "lib";
2019
- //
2020
- // Depending on our environment, importing is done differently:
2021
- // In the browser, it's an XHR request, in Node, it would be a
2022
- // file-system operation. The function used for importing is
2023
- // stored in `import`, which we pass to the Import constructor.
2024
- //
2025
- 'import': function () {
2026
- let path;
2027
- let features;
2028
- const index = parserInput.i;
2029
-
2030
- const dir = parserInput.$re(/^@import\s+/);
2031
-
2032
- if (dir) {
2033
- const options = (dir ? this.importOptions() : null) || {};
2034
-
2035
- if ((path = this.entities.quoted() || this.entities.url())) {
2036
- features = this.mediaFeatures({});
2037
-
2038
- if (!parserInput.$char(';')) {
2039
- parserInput.i = index;
2040
- error('missing semi-colon or unrecognised media features on import');
2041
- }
2042
- features = features && new(tree.Value)(features);
2043
- return new(tree.Import)(path, features, options, index + currentIndex, fileInfo);
2044
- }
2045
- else {
2046
- parserInput.i = index;
2047
- error('malformed import statement');
2048
- }
2049
- }
2050
- },
2051
-
2052
- importOptions: function() {
2053
- let o;
2054
- const options = {};
2055
- let optionName;
2056
- let value;
2057
-
2058
- // list of options, surrounded by parens
2059
- if (!parserInput.$char('(')) { return null; }
2060
- do {
2061
- o = this.importOption();
2062
- if (o) {
2063
- optionName = o;
2064
- value = true;
2065
- switch (optionName) {
2066
- case 'css':
2067
- optionName = 'less';
2068
- value = false;
2069
- break;
2070
- case 'once':
2071
- optionName = 'multiple';
2072
- value = false;
2073
- break;
2074
- }
2075
- options[optionName] = value;
2076
- if (!parserInput.$char(',')) { break; }
2077
- }
2078
- } while (o);
2079
- expectChar(')');
2080
- return options;
2081
- },
2082
-
2083
- importOption: function() {
2084
- const opt = parserInput.$re(/^(less|css|multiple|once|inline|reference|optional)/);
2085
- if (opt) {
2086
- return opt[1];
2087
- }
2088
- },
2089
-
2090
- mediaFeature: function (syntaxOptions) {
2091
- const entities = this.entities;
2092
- const nodes = [];
2093
- let e;
2094
- let p;
2095
- let rangeP;
2096
- let spacing = false;
2097
- const cssKeyword = () => {
2098
- const k = parserInput.$re(/^(?:--|-?(?:[_a-zA-Z0-9]|[^\0-\x7F]|\\[0-9a-fA-F]{1,6}\s?|\\[^\n\r\f0-9a-fA-F]))(?:[-_a-zA-Z0-9]|[^\0-\x7F]|\\[0-9a-fA-F]{1,6}\s?|\\[^\n\r\f0-9a-fA-F])*/);
2099
- if (k) {
2100
- return tree.Color.fromKeyword(k) || new(tree.Keyword)(k);
2101
- }
2102
- };
2103
- parserInput.save();
2104
- do {
2105
- parserInput.save();
2106
- if (parserInput.$re(/^(?:--|-?(?:[_a-zA-Z0-9]|[^\0-\x7F]|\\[0-9a-fA-F]{1,6}\s?|\\[^\n\r\f0-9a-fA-F]))(?:[-_a-zA-Z0-9]|[^\0-\x7F]|\\[0-9a-fA-F]{1,6}\s?|\\[^\n\r\f0-9a-fA-F])*\s+\(/)) {
2107
- spacing = true;
2108
- }
2109
- parserInput.restore();
2110
-
2111
- e = entities.declarationCall.bind(this)() || cssKeyword() || entities.keyword() || entities.variableCurly();
2112
- if (!e) {
2113
- const varIndex = parserInput.i;
2114
- const bareVariable = entities.variable();
2115
- if (bareVariable) {
2116
- warnBareAtRuleVariable(varIndex);
2117
- e = bareVariable;
2118
- } else {
2119
- e = entities.mixinLookup();
2120
- }
2121
- }
2122
- if (e) {
2123
- nodes.push(e);
2124
- if (e.type === 'Variable' ||
2125
- (e.type === 'Keyword' && /^(and|or|not|only)$/i.test(e.value))) {
2126
- spacing = true;
2127
- }
2128
- } else if (parserInput.$char('(')) {
2129
- let closed = false;
2130
- p = this.property();
2131
- parserInput.save();
2132
- if (!p && syntaxOptions.queryInParens && parserInput.$re(/^(?:[^()]|\([^()]*\))*\s*([<>]=|<=|>=|[<>]|=)/)) {
2133
- parserInput.restore();
2134
- p = this.condition();
2135
-
2136
- parserInput.save();
2137
- rangeP = this.atomicCondition(null, p.rvalue);
2138
- if (!rangeP) {
2139
- parserInput.restore();
2140
- }
2141
- } else {
2142
- parserInput.restore();
2143
- parserInput.save();
2144
- e = this.value();
2145
- if (e && parserInput.$char(')')) {
2146
- closed = true;
2147
- parserInput.forget();
2148
- } else {
2149
- parserInput.restore();
2150
- e = this.mediaFeature(syntaxOptions);
2151
- }
2152
- }
2153
- if (!closed && parserInput.$char(')')) {
2154
- closed = true;
2155
- }
2156
- if (closed) {
2157
- if (p && !e) {
2158
- const paren = new (tree.Paren)(new (tree.QueryInParens)(p.op, p.lvalue, p.rvalue, rangeP ? rangeP.op : null, rangeP ? rangeP.rvalue : null, p._index));
2159
- if (!spacing) {
2160
- paren.noSpacing = true;
2161
- }
2162
- nodes.push(paren);
2163
- e = p;
2164
- } else if (p && e) {
2165
- nodes.push(new (tree.Paren)(new (tree.Declaration)(p, e, null, null, parserInput.i + currentIndex, fileInfo, true)));
2166
- if (!spacing) {
2167
- nodes[nodes.length - 1].noSpacing = true;
2168
- }
2169
- spacing = false;
2170
- } else if (e) {
2171
- const paren = new(tree.Paren)(e);
2172
- if (!spacing) {
2173
- paren.noSpacing = true;
2174
- }
2175
- nodes.push(paren);
2176
- spacing = false;
2177
- } else {
2178
- error('badly formed media feature definition');
2179
- }
2180
- } else {
2181
- error('Missing closing \')\'', 'Parse');
2182
- }
2183
- }
2184
- } while (e);
2185
-
2186
- parserInput.forget();
2187
- if (nodes.length > 0) {
2188
- const expression = new(tree.Expression)(nodes);
2189
- if (nodes.length === 2
2190
- && (nodes[0].type === 'Keyword' || nodes[0].type === 'Variable')
2191
- && nodes[1].type === 'Paren'
2192
- && nodes[1].noSpacing) {
2193
- expression.noSpacing = true;
2194
- }
2195
- return expression;
2196
- }
2197
- },
2198
-
2199
- mediaFeatures: function (syntaxOptions) {
2200
- const entities = this.entities;
2201
- const features = [];
2202
- let e;
2203
- do {
2204
- e = this.mediaFeature(syntaxOptions);
2205
- if (e) {
2206
- features.push(e);
2207
- if (!parserInput.$char(',')) { break; }
2208
- else if (!features[features.length - 1].noSpacing) {
2209
- features[features.length - 1].noSpacing = false;
2210
- }
2211
- } else {
2212
- e = entities.variableCurly();
2213
- if (!e) {
2214
- const varIndex = parserInput.i;
2215
- const bareVariable = entities.variable();
2216
- if (bareVariable) {
2217
- warnBareAtRuleVariable(varIndex);
2218
- e = bareVariable;
2219
- } else {
2220
- e = entities.mixinLookup();
2221
- }
2222
- }
2223
- if (e) {
2224
- features.push(e);
2225
- if (!parserInput.$char(',')) { break; }
2226
- else if (!features[features.length - 1].noSpacing) {
2227
- features[features.length - 1].noSpacing = false;
2228
- }
2229
- }
2230
- }
2231
- } while (e);
2232
-
2233
- return features.length > 0 ? features : null;
2234
- },
2235
-
2236
- prepareAndGetNestableAtRule: function (treeType, index, debugInfo, syntaxOptions) {
2237
- const features = this.mediaFeatures(syntaxOptions);
2238
-
2239
- const rules = this.block();
2240
-
2241
- if (!rules) {
2242
- error('media definitions require block statements after any features');
2243
- }
2244
-
2245
- parserInput.forget();
2246
-
2247
- const atRule = new (treeType)(rules, features, index + currentIndex, fileInfo);
2248
- if (context.dumpLineNumbers) {
2249
- atRule.debugInfo = debugInfo;
2250
- }
2251
-
2252
- return atRule;
2253
- },
2254
-
2255
- nestableAtRule: function () {
2256
- let debugInfo;
2257
- const index = parserInput.i;
2258
-
2259
- if (context.dumpLineNumbers) {
2260
- debugInfo = getDebugInfo(index);
2261
- }
2262
- parserInput.save();
2263
-
2264
- if (parserInput.$peekChar('@')) {
2265
- if (parserInput.$str('@media')) {
2266
- return this.prepareAndGetNestableAtRule(tree.Media, index, debugInfo, MediaSyntaxOptions);
2267
- }
2268
-
2269
- if (parserInput.$str('@container')) {
2270
- return this.prepareAndGetNestableAtRule(tree.Container, index, debugInfo, ContainerSyntaxOptions);
2271
- }
2272
- }
2273
-
2274
- parserInput.restore();
2275
- },
2276
-
2277
- //
2278
-
2279
- // A @plugin directive, used to import plugins dynamically.
2280
- //
2281
- // @plugin (args) "lib";
2282
- //
2283
- plugin: function () {
2284
- let path;
2285
- let args;
2286
- let options;
2287
- const index = parserInput.i;
2288
- const dir = parserInput.$re(/^@plugin\s+/);
2289
-
2290
- if (dir) {
2291
- warn('The @plugin directive is deprecated and will be replaced in Less 5.x. Use --plugin CLI option or the programmatic plugin API instead.', index, 'DEPRECATED', 'at-plugin');
2292
- args = this.pluginArgs();
2293
-
2294
- if (args) {
2295
- options = {
2296
- pluginArgs: args,
2297
- isPlugin: true
2298
- };
2299
- }
2300
- else {
2301
- options = { isPlugin: true };
2302
- }
2303
-
2304
- if ((path = this.entities.quoted() || this.entities.url())) {
2305
-
2306
- if (!parserInput.$char(';')) {
2307
- parserInput.i = index;
2308
- error('missing semi-colon on @plugin');
2309
- }
2310
- return new(tree.Import)(path, null, options, index + currentIndex, fileInfo);
2311
- }
2312
- else {
2313
- parserInput.i = index;
2314
- error('malformed @plugin statement');
2315
- }
2316
- }
2317
- },
2318
-
2319
- pluginArgs: function() {
2320
- // list of options, surrounded by parens
2321
- parserInput.save();
2322
- if (!parserInput.$char('(')) {
2323
- parserInput.restore();
2324
- return null;
2325
- }
2326
- const args = parserInput.$re(/^\s*([^);]+)\)\s*/);
2327
- if (args[1]) {
2328
- parserInput.forget();
2329
- return args[1].trim();
2330
- }
2331
- else {
2332
- parserInput.restore();
2333
- return null;
2334
- }
2335
- },
2336
- /**
2337
- * An entity in a non-value at-rule position (an at-rule identifier,
2338
- * name, or keyword-list item — e.g. the name in `@keyframes @foo`).
2339
- * `@{foo}` interpolation is the supported form; a bare `@foo` still
2340
- * resolves but is deprecated.
2341
- */
2342
- atRuleEntity: function () {
2343
- const curly = this.entities.variableCurly();
2344
- if (curly) { return curly; }
2345
- const index = parserInput.i;
2346
- const e = this.entity();
2347
- if (isBareVariableReference(e)) {
2348
- warnBareAtRuleVariable(index);
2349
- }
2350
- return e;
2351
- },
2352
- atruleUnknown: function (value, name, hasBlock) {
2353
- value = this.permissiveValue(/^[{;]/, true);
2354
- hasBlock = (parserInput.currentChar() === '{');
2355
- if (!value) {
2356
- if (!hasBlock && parserInput.currentChar() !== ';') {
2357
- error(''.concat(name, ' rule is missing block or ending semi-colon'));
2358
- }
2359
- }
2360
- else if (!value.value) {
2361
- value = null;
2362
- }
2363
- return [value, hasBlock];
2364
- },
2365
- atruleBlock: function (rules, value, isRooted, isKeywordList) {
2366
- rules = this.blockRuleset();
2367
- parserInput.save();
2368
- if (!rules && !isRooted) {
2369
- value = this.atRuleEntity();
2370
- rules = this.blockRuleset();
2371
- }
2372
- if (!rules && !isRooted) {
2373
- parserInput.restore();
2374
- var e = [];
2375
- value = this.atRuleEntity();
2376
- while (parserInput.$char(',')) {
2377
- e.push(value);
2378
- value = this.atRuleEntity();
2379
- }
2380
- if (value && e.length > 0) {
2381
- e.push(value);
2382
- value = e;
2383
- isKeywordList = true;
2384
- }
2385
- else {
2386
- rules = this.blockRuleset();
2387
- }
2388
- }
2389
- else {
2390
- parserInput.forget();
2391
- }
2392
-
2393
- return [rules, value, isKeywordList];
2394
- },
2395
- //
2396
- // A CSS AtRule
2397
- //
2398
- // @charset "utf-8";
2399
- //
2400
- atrule: function () {
2401
- const index = parserInput.i;
2402
- let name;
2403
- let value;
2404
- let rules;
2405
- let nonVendorSpecificName;
2406
- let hasIdentifier;
2407
- let hasExpression;
2408
- let hasUnknown;
2409
- let hasBlock = true;
2410
- let isRooted = true;
2411
- let isKeywordList = false;
2412
-
2413
- if (parserInput.currentChar() !== '@') { return; }
2414
-
2415
- value = this['import']() || this.plugin() || this.nestableAtRule();
2416
- if (value) {
2417
- return value;
2418
- }
2419
-
2420
- parserInput.save();
2421
-
2422
- name = parserInput.$re(/^@[a-z-]+/);
2423
-
2424
- if (!name) { return; }
2425
-
2426
- nonVendorSpecificName = name;
2427
- if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) {
2428
- nonVendorSpecificName = `@${name.slice(name.indexOf('-', 2) + 1)}`;
2429
- }
2430
-
2431
- switch (nonVendorSpecificName) {
2432
- case '@charset':
2433
- hasIdentifier = true;
2434
- hasBlock = false;
2435
- break;
2436
- case '@namespace':
2437
- hasExpression = true;
2438
- hasBlock = false;
2439
- break;
2440
- case '@keyframes':
2441
- case '@counter-style':
2442
- hasIdentifier = true;
2443
- break;
2444
- case '@document':
2445
- case '@supports':
2446
- hasUnknown = true;
2447
- isRooted = false;
2448
- break;
2449
- case '@starting-style':
2450
- isRooted = false;
2451
- break;
2452
- case '@layer':
2453
- isRooted = false;
2454
- break;
2455
- default:
2456
- hasUnknown = true;
2457
- break;
2458
- }
2459
-
2460
- parserInput.commentStore.length = 0;
2461
-
2462
- if (hasIdentifier) {
2463
- value = this.atRuleEntity();
2464
- if (!value) {
2465
- error(`expected ${name} identifier`);
2466
- }
2467
- if (nonVendorSpecificName === '@charset') {
2468
- warnDynamicCharset(value, index);
2469
- }
2470
- } else if (hasExpression) {
2471
- // `@namespace` may carry an interpolated `@{ns}` prefix (or a
2472
- // deprecated bare `@ns`). Parse that prefix directly so `@{ns}`
2473
- // is accepted here without treating value positions as
2474
- // interpolation contexts, then read the namespace URL.
2475
- let prefix = this.entities.variableCurly();
2476
- if (!prefix && parserInput.peek(/^@@?[\w-]/)) {
2477
- const prefixIndex = parserInput.i;
2478
- prefix = this.entities.variable();
2479
- if (prefix) { warnBareAtRuleVariable(prefixIndex); }
2480
- }
2481
- value = this.expression();
2482
- if (prefix) {
2483
- value = value ? new(tree.Expression)([prefix, ...value.value]) : prefix;
2484
- }
2485
- if (!value) {
2486
- error(`expected ${name} expression`);
2487
- }
2488
- } else if (hasUnknown) {
2489
- const unknownPackage = this.atruleUnknown(value, name, hasBlock);
2490
- value = unknownPackage[0];
2491
- hasBlock = unknownPackage[1];
2492
- }
2493
-
2494
- if (hasBlock) {
2495
- let blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
2496
- rules = blockPackage[0];
2497
- value = blockPackage[1];
2498
- isKeywordList = blockPackage[2];
2499
-
2500
- if (!rules && !hasUnknown) {
2501
- parserInput.restore();
2502
- name = parserInput.$re(/^@[a-z-]+/);
2503
- const unknownPackage = this.atruleUnknown(value, name, hasBlock);
2504
- value = unknownPackage[0];
2505
- hasBlock = unknownPackage[1];
2506
- if (hasBlock) {
2507
- blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
2508
- rules = blockPackage[0];
2509
- value = blockPackage[1];
2510
- isKeywordList = blockPackage[2];
2511
- }
2512
- }
2513
- }
2514
-
2515
- if (rules || isKeywordList || (!hasBlock && value && parserInput.$char(';'))) {
2516
- parserInput.forget();
2517
- return new(tree.AtRule)(name, value, rules, index + currentIndex, fileInfo,
2518
- context.dumpLineNumbers ? getDebugInfo(index) : null,
2519
- isRooted
2520
- );
2521
- }
2522
-
2523
- parserInput.restore('at-rule options not recognised');
2524
- },
2525
-
2526
- //
2527
- // A Value is a comma-delimited list of Expressions
2528
- //
2529
- // font-family: Baskerville, Georgia, serif;
2530
- //
2531
- // In a Rule, a Value represents everything after the `:`,
2532
- // and before the `;`.
2533
- //
2534
- value: function () {
2535
- let e;
2536
- const expressions = [];
2537
- const index = parserInput.i;
2538
-
2539
- do {
2540
- e = this.expression();
2541
- if (e) {
2542
- expressions.push(e);
2543
- if (!parserInput.$char(',')) { break; }
2544
- }
2545
- } while (e);
2546
-
2547
- if (expressions.length > 0) {
2548
- return new(tree.Value)(expressions, index + currentIndex);
2549
- }
2550
- },
2551
- important: function () {
2552
- if (parserInput.currentChar() === '!') {
2553
- return parserInput.$re(/^! *important/);
2554
- }
2555
- },
2556
- sub: function () {
2557
- let a;
2558
- let e;
2559
-
2560
- parserInput.save();
2561
- if (parserInput.$char('(')) {
2562
- a = this.addition();
2563
- if (a && parserInput.$char(')')) {
2564
- parserInput.forget();
2565
- e = new(tree.Expression)([a]);
2566
- e.parens = true;
2567
- return e;
2568
- }
2569
- parserInput.restore('Expected \')\'');
2570
- return;
2571
- }
2572
- parserInput.restore();
2573
- },
2574
- colorOperand: function () {
2575
- parserInput.save();
2576
-
2577
- // hsl or rgb or lch operand
2578
- const match = parserInput.$re(/^([lchrgbs])(?=\s|[,/*)]|$)/);
2579
- if (match) {
2580
- parserInput.forget();
2581
- return new tree.Keyword(match[1]);
2582
- }
2583
-
2584
- parserInput.restore();
2585
- },
2586
- multiplication: function () {
2587
- let m;
2588
- let a;
2589
- let op;
2590
- let operation;
2591
- let isSpaced;
2592
- m = this.operand();
2593
- if (m) {
2594
- isSpaced = parserInput.isWhitespace(-1);
2595
- while (true) {
2596
- if (parserInput.peek(/^\/[*/]/)) {
2597
- break;
2598
- }
2599
-
2600
- parserInput.save();
2601
-
2602
- op = parserInput.$char('/') || parserInput.$char('*');
2603
- if (!op) {
2604
- let index = parserInput.i;
2605
- op = parserInput.$str('./');
2606
- if (op) {
2607
- warn('./ operator is deprecated', index, 'DEPRECATED', 'dot-slash-operator');
2608
- }
2609
- }
2610
-
2611
- if (!op) { parserInput.forget(); break; }
2612
-
2613
- a = this.operand();
2614
-
2615
- if (!a) { parserInput.restore(); break; }
2616
- parserInput.forget();
2617
-
2618
- m.parensInOp = true;
2619
- a.parensInOp = true;
2620
- operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
2621
- isSpaced = parserInput.isWhitespace(-1);
2622
- }
2623
- return operation || m;
2624
- }
2625
- },
2626
- addition: function () {
2627
- let m;
2628
- let a;
2629
- let op;
2630
- let operation;
2631
- let isSpaced;
2632
- m = this.multiplication();
2633
- if (m) {
2634
- isSpaced = parserInput.isWhitespace(-1);
2635
- while (true) {
2636
- op = parserInput.$re(/^[-+]\s+/) || (!isSpaced && (parserInput.$char('+') || parserInput.$char('-')));
2637
- if (!op) {
2638
- break;
2639
- }
2640
- a = this.multiplication();
2641
- if (!a) {
2642
- break;
2643
- }
2644
-
2645
- m.parensInOp = true;
2646
- a.parensInOp = true;
2647
- operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
2648
- isSpaced = parserInput.isWhitespace(-1);
2649
- }
2650
- return operation || m;
2651
- }
2652
- },
2653
- conditions: function () {
2654
- let a;
2655
- let b;
2656
- const index = parserInput.i;
2657
- let condition;
2658
-
2659
- a = this.condition(true);
2660
- if (a) {
2661
- while (true) {
2662
- if (!parserInput.peek(/^,\s*(not\s*)?\(/) || !parserInput.$char(',')) {
2663
- break;
2664
- }
2665
- b = this.condition(true);
2666
- if (!b) {
2667
- break;
2668
- }
2669
- condition = new(tree.Condition)('or', condition || a, b, index + currentIndex);
2670
- }
2671
- return condition || a;
2672
- }
2673
- },
2674
- condition: function (needsParens, allowConditionOperands) {
2675
- let result;
2676
- let logical;
2677
- let next;
2678
- function or() {
2679
- return parserInput.$str('or');
2680
- }
2681
-
2682
- result = this.conditionAnd(needsParens, allowConditionOperands);
2683
- if (!result) {
2684
- return ;
2685
- }
2686
- logical = or();
2687
- if (logical) {
2688
- next = this.condition(needsParens, allowConditionOperands);
2689
- if (next) {
2690
- result = new(tree.Condition)(logical, result, next);
2691
- } else {
2692
- return ;
2693
- }
2694
- }
2695
- return result;
2696
- },
2697
- conditionAnd: function (needsParens, allowConditionOperands) {
2698
- let result;
2699
- let logical;
2700
- let next;
2701
- const self = this;
2702
- function insideCondition() {
2703
- const cond = self.negatedCondition(needsParens, allowConditionOperands) || self.parenthesisCondition(needsParens, allowConditionOperands);
2704
- if (!cond && !needsParens) {
2705
- return self.atomicCondition(needsParens);
2706
- }
2707
- return cond;
2708
- }
2709
- function and() {
2710
- return parserInput.$str('and');
2711
- }
2712
-
2713
- result = insideCondition();
2714
- if (!result) {
2715
- return ;
2716
- }
2717
- if (allowConditionOperands) {
2718
- result = this.atomicCondition(needsParens, result, allowConditionOperands) || result;
2719
- }
2720
- logical = and();
2721
- if (logical) {
2722
- next = this.conditionAnd(needsParens, allowConditionOperands);
2723
- if (next) {
2724
- result = new(tree.Condition)(logical, result, next);
2725
- } else {
2726
- return ;
2727
- }
2728
- }
2729
- return result;
2730
- },
2731
- negatedCondition: function (needsParens, allowConditionOperands) {
2732
- if (parserInput.$str('not')) {
2733
- const result = this.parenthesisCondition(needsParens, allowConditionOperands);
2734
- if (result) {
2735
- result.negate = !result.negate;
2736
- return result;
2737
- }
2738
-
2739
- // Allow simple bare values (keyword/variable) without parens,
2740
- // e.g., `not false` or `not @var`.
2741
- // Complex conditions (comparisons, function calls) require parentheses.
2742
- const entities = this.entities;
2743
- const index = parserInput.i;
2744
- const a = entities.keyword() || entities.variable() || entities.quoted() || entities.mixinLookup();
2745
- if (a) {
2746
- return new(tree.Condition)('=', a, new(tree.Keyword)('true'), index + currentIndex, true);
2747
- }
2748
- }
2749
- },
2750
- parenthesisCondition: function (needsParens, allowConditionOperands) {
2751
- function tryConditionFollowedByParenthesis(me) {
2752
- let body;
2753
- parserInput.save();
2754
- body = me.condition(needsParens, allowConditionOperands);
2755
- if (!body) {
2756
- parserInput.restore();
2757
- return ;
2758
- }
2759
- if (!parserInput.$char(')')) {
2760
- parserInput.restore();
2761
- return ;
2762
- }
2763
- parserInput.forget();
2764
- return body;
2765
- }
2766
-
2767
- let body;
2768
- parserInput.save();
2769
- if (!parserInput.$str('(')) {
2770
- parserInput.restore();
2771
- return ;
2772
- }
2773
- body = tryConditionFollowedByParenthesis(this);
2774
- if (body) {
2775
- parserInput.forget();
2776
- return body;
2777
- }
2778
-
2779
- body = this.atomicCondition(needsParens);
2780
- if (!body) {
2781
- parserInput.restore();
2782
- return ;
2783
- }
2784
- if (!parserInput.$char(')')) {
2785
- parserInput.restore(`expected ')' got '${parserInput.currentChar()}'`);
2786
- return ;
2787
- }
2788
- parserInput.forget();
2789
- return body;
2790
- },
2791
- atomicCondition: function (needsParens, preparsedCond, allowConditionOperands) {
2792
- const entities = this.entities;
2793
- const index = parserInput.i;
2794
- let a;
2795
- let b;
2796
- let c;
2797
- let op;
2798
-
2799
- const cond = (function() {
2800
- return (allowConditionOperands && this.parenthesisCondition(needsParens)) || this.addition() || entities.keyword() || entities.quoted() || entities.mixinLookup();
2801
- }).bind(this)
2802
-
2803
- if (preparsedCond) {
2804
- a = preparsedCond;
2805
- } else {
2806
- a = cond();
2807
- }
2808
-
2809
- if (a) {
2810
- if (parserInput.$char('>')) {
2811
- if (parserInput.$char('=')) {
2812
- op = '>=';
2813
- } else {
2814
- op = '>';
2815
- }
2816
- } else
2817
- if (parserInput.$char('<')) {
2818
- if (parserInput.$char('=')) {
2819
- op = '<=';
2820
- } else {
2821
- op = '<';
2822
- }
2823
- } else
2824
- if (parserInput.$char('=')) {
2825
- if (parserInput.$char('>')) {
2826
- op = '=>';
2827
- } else if (parserInput.$char('<')) {
2828
- op = '=<';
2829
- } else {
2830
- op = '=';
2831
- }
2832
- }
2833
- if (op) {
2834
- b = cond();
2835
- if (b) {
2836
- c = new(tree.Condition)(op, a, b, index + currentIndex, false);
2837
- } else {
2838
- error('expected expression');
2839
- }
2840
- } else if (!preparsedCond) {
2841
- c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index + currentIndex, false);
2842
- }
2843
- return c;
2844
- }
2845
- },
2846
-
2847
- //
2848
- // An operand is anything that can be part of an operation,
2849
- // such as a Color, or a Variable
2850
- //
2851
- operand: function () {
2852
- const entities = this.entities;
2853
- let negate;
2854
-
2855
- if (parserInput.peek(/^-[@$(]/)) {
2856
- negate = parserInput.$char('-');
2857
- }
2858
-
2859
- let o = this.sub() || entities.dimension() ||
2860
- entities.color() || entities.variable() ||
2861
- entities.property() || entities.call() ||
2862
- entities.quoted(true) || entities.colorKeyword() ||
2863
- this.colorOperand() || entities.mixinLookup();
2864
-
2865
- if (negate) {
2866
- o.parensInOp = true;
2867
- o = new(tree.Negative)(o);
2868
- }
2869
-
2870
- return o;
2871
- },
2872
-
2873
- //
2874
- // Expressions either represent mathematical operations,
2875
- // or white-space delimited Entities.
2876
- //
2877
- // 1px solid black
2878
- // @var * 2
2879
- //
2880
- expression: function () {
2881
- const entities = [];
2882
- let e;
2883
- let delim;
2884
- const index = parserInput.i;
2885
-
2886
- do {
2887
- e = this.comment();
2888
- if (e && !e.isLineComment) {
2889
- entities.push(e);
2890
- continue;
2891
- }
2892
- e = this.addition() || this.entity();
2893
-
2894
- if (e instanceof tree.Comment) {
2895
- e = null;
2896
- }
2897
-
2898
- if (e) {
2899
- entities.push(e);
2900
- // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
2901
- if (!parserInput.peek(/^\/[/*]/)) {
2902
- delim = parserInput.$char('/');
2903
- if (delim) {
2904
- entities.push(new(tree.Anonymous)(delim, index + currentIndex));
2905
- }
2906
- }
2907
- }
2908
- } while (e);
2909
- if (entities.length > 0) {
2910
- return new(tree.Expression)(entities);
2911
- }
2912
- },
2913
- property: function () {
2914
- const name = parserInput.$re(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/);
2915
- if (name) {
2916
- return name[1];
2917
- }
2918
- },
2919
- ruleProperty: function () {
2920
- let name = [];
2921
- const index = [];
2922
- let s;
2923
- let k;
2924
-
2925
- parserInput.save();
2926
-
2927
- const simpleProperty = parserInput.$re(/^([_a-zA-Z0-9-]+)\s*:/);
2928
- if (simpleProperty) {
2929
- name = [new(tree.Keyword)(simpleProperty[1])];
2930
- parserInput.forget();
2931
- return name;
2932
- }
2933
-
2934
- function match(re) {
2935
- const i = parserInput.i;
2936
- const chunk = parserInput.$re(re);
2937
- if (chunk) {
2938
- index.push(i);
2939
- return name.push(chunk[1]);
2940
- }
2941
- }
2942
-
2943
- match(/^(\*?)/);
2944
- while (true) {
2945
- if (!match(RULE_PROPERTY_PARTICLE)) {
2946
- break;
2947
- }
2948
- }
2949
-
2950
- if ((name.length > 1) && match(/^((?:\+_|\+)?)\s*:/)) {
2951
- parserInput.forget();
2952
-
2953
- // at last, we have the complete match now. move forward,
2954
- // convert name particles to tree objects and return:
2955
- if (name[0] === '') {
2956
- name.shift();
2957
- index.shift();
2958
- }
2959
- for (k = 0; k < name.length; k++) {
2960
- s = name[k];
2961
- const sigil = s.charAt(0);
2962
- if (sigil !== '@' && sigil !== '$') {
2963
- name[k] = new(tree.Keyword)(s);
2964
- continue;
2965
- }
2966
- // `@{name}` / `@{name[key]}` — strip the sigil and braces, then
2967
- // let the shared resolver decide between a plain reference and
2968
- // a lookup so this path cannot drift from the others.
2969
- const raw = s.slice(2, -1);
2970
- if (sigil === '@') {
2971
- warnNumericVariableName(splitLookups(raw).name, index[k]);
2972
- warnDashOnlyVariableName(splitLookups(raw).name, index[k]);
2973
- name[k] = resolveInterpolatedVariable(raw, index[k] + currentIndex, fileInfo);
2974
- } else {
2975
- name[k] = resolveInterpolatedProperty(raw, index[k] + currentIndex, fileInfo);
2976
- }
2977
- }
2978
- return name;
2979
- }
2980
- parserInput.restore();
2981
- }
2982
- }
2983
- };
2984
- };
2985
- Parser.serializeVars = vars => {
2986
- let s = '';
2987
-
2988
- for (const name in vars) {
2989
- if (Object.hasOwnProperty.call(vars, name)) {
2990
- const value = vars[name];
2991
- s += `${((name[0] === '@') ? '' : '@') + name}: ${value}${(String(value).slice(-1) === ';') ? '' : ';'}`;
2992
- }
2993
- }
2994
-
2995
- return s;
2996
- };
2997
-
2998
- export default Parser;