less 4.8.1 → 5.0.0-alpha.1

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