@thejaredwilcurt/csslop 0.0.38 → 0.0.39

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.
@@ -0,0 +1,826 @@
1
+ /**
2
+ * @file Parses a string of CSS to an abstract syntax tree (AST).
3
+ */
4
+
5
+ import {
6
+ AT_RULE_BODY,
7
+ findAtRuleDefinition
8
+ } from './at-rules.js';
9
+ import {
10
+ createLineIndex,
11
+ createPosition,
12
+ resolveSourceLocation
13
+ } from './positions.js';
14
+ import {
15
+ CHARACTER_CODE,
16
+ characterCodeAt,
17
+ consumeCloseBrace,
18
+ consumeColon,
19
+ consumeComma,
20
+ consumeOpenBrace,
21
+ consumePattern,
22
+ consumeTo,
23
+ createScanner,
24
+ hasMoreInput,
25
+ peekPattern,
26
+ skipSemicolonsAndWhitespace,
27
+ skipWhitespace
28
+ } from './scanner.js';
29
+ import {
30
+ findTopLevelDelimiter,
31
+ splitTopLevel
32
+ } from './source-search.js';
33
+
34
+ /**
35
+ * Matches a comment, from the current position, up to the first end of a
36
+ * comment. A comment that is never closed runs to the end of the stylesheet,
37
+ * which lets the parser report it and still move on.
38
+ *
39
+ * @type {RegExp}
40
+ */
41
+ // The comment opener, the fewest characters of any kind, then a closer or the end of input
42
+ const COMMENT_PATTERN = /\/\*[^]*?(?:\*\/|$)/y;
43
+
44
+ /**
45
+ * Matches every comment in a piece of text, so that comments can be removed
46
+ * from the selectors and values they were written between.
47
+ *
48
+ * @type {RegExp}
49
+ */
50
+ // The comment opener, the fewest characters of any kind, then a closer or the end of the text
51
+ const COMMENT_SEARCH_PATTERN = /\/\*[^]*?(?:\*\/|$)/g;
52
+
53
+ /**
54
+ * Matches the property name of a declaration. Beyond the characters of an
55
+ * identifier, the name may hold the punctuation of the hacks old stylesheets
56
+ * use to hide a declaration from a browser, and may end with the bracketed
57
+ * suffix of a vendor extension.
58
+ *
59
+ * @type {RegExp}
60
+ */
61
+ // An optional leading asterisk, the name characters, an optional bracketed suffix, then the whitespace before the colon
62
+ const PROPERTY_NAME_PATTERN = /(\*?[-#/*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*/y;
63
+
64
+ /**
65
+ * Matches the name of an at-rule, which is what decides how the rest of it is
66
+ * read.
67
+ *
68
+ * @type {RegExp}
69
+ */
70
+ // The at-sign followed by an identifier
71
+ const AT_RULE_NAME_PATTERN = /@([-\w]+)/y;
72
+
73
+ /**
74
+ * Matches one stop of a keyframe's selector, which is either a percentage or
75
+ * one of the `from` and `to` keywords.
76
+ *
77
+ * @type {RegExp}
78
+ */
79
+ // A number with an optional percent sign, or a keyword, then the whitespace behind it
80
+ const KEYFRAME_STOP_PATTERN = /((?:\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/y;
81
+
82
+ /**
83
+ * How the parser reads the body of a block, which differs by what the block
84
+ * is allowed to hold.
85
+ *
86
+ * @type {object}
87
+ */
88
+ const BLOCK_CONTENTS = {
89
+ declarationsOnly: {
90
+ atRules: false,
91
+ nestedRules: false
92
+ },
93
+ pageBody: {
94
+ atRules: true,
95
+ nestedRules: false
96
+ },
97
+ rulesAndDeclarations: {
98
+ atRules: true,
99
+ nestedRules: true
100
+ }
101
+ };
102
+
103
+ /**
104
+ * Creates everything the parse of one stylesheet needs: the reader that walks
105
+ * the text, the line index its positions are resolved against, and the list
106
+ * the errors of a silent parse are collected in.
107
+ *
108
+ * @param {string} source The raw CSS text to parse.
109
+ * @param {object} options The options the caller parsed with.
110
+ * @return {object} The state shared by every step of the parse.
111
+ */
112
+ function createParserState (source, options) {
113
+ return {
114
+ source,
115
+ scanner: createScanner(source),
116
+ sourceName: options.source || '',
117
+ silent: Boolean(options.silent),
118
+ preserveFormatting: Boolean(options.preserveFormatting),
119
+ lineStartOffsets: createLineIndex(source),
120
+ parsingErrors: []
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Reports that the stylesheet does not say what the parser expected it to. A
126
+ * silent parse collects the error and recovers, while any other parse stops
127
+ * at the first thing it cannot read.
128
+ *
129
+ * @param {object} state The state of the parse.
130
+ * @param {string} message What the parser expected to find.
131
+ */
132
+ function recordError (state, message) {
133
+ const location = resolveSourceLocation(state.lineStartOffsets, state.scanner.index);
134
+ const error = new Error(state.sourceName + ':' + location.line + ':' + location.column + ': ' + message);
135
+ Object.assign(error, {
136
+ reason: message,
137
+ filename: state.sourceName,
138
+ line: location.line,
139
+ column: location.column,
140
+ source: state.source
141
+ });
142
+ if (!state.silent) {
143
+ throw error;
144
+ }
145
+ state.parsingErrors.push(error);
146
+ }
147
+
148
+ /**
149
+ * Finishes a node by recording the span of text it was parsed from, then
150
+ * steps over the whitespace behind it so that the next node starts at the
151
+ * text it is actually made of.
152
+ *
153
+ * @param {object} state The state of the parse.
154
+ * @param {object} node The node that was parsed.
155
+ * @param {number} startOffset The offset the node began at.
156
+ * @return {object} The finished node.
157
+ */
158
+ function finishNode (state, node, startOffset) {
159
+ node.position = createPosition(
160
+ state.lineStartOffsets,
161
+ startOffset,
162
+ state.scanner.index,
163
+ state.sourceName
164
+ );
165
+ skipWhitespace(state.scanner);
166
+ return node;
167
+ }
168
+
169
+ /**
170
+ * Records the raw text a node's prelude was written with, which is everything
171
+ * in front of its block, so that the node can be written back out exactly as
172
+ * it was read.
173
+ *
174
+ * @param {object} state The state of the parse.
175
+ * @param {object} node The node the prelude belongs to.
176
+ * @param {number} startOffset The offset the node began at.
177
+ * @param {number} preludeEndOffset The offset the prelude ended at.
178
+ */
179
+ function addRawPrelude (state, node, startOffset, preludeEndOffset) {
180
+ if (state.preserveFormatting) {
181
+ node.rawPrelude = state.source.slice(startOffset, preludeEndOffset);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Fills the gaps between the nodes of a block with the whitespace that was
187
+ * written there, so that a stylesheet can be rebuilt character for character.
188
+ * The gaps hold more than spaces: the semicolon behind a declaration is part
189
+ * of what separates it from the declaration that follows.
190
+ *
191
+ * @param {object} state The state of the parse.
192
+ * @param {Array} nodes The nodes of the block, in source order.
193
+ * @param {number} containerStart The offset the block's contents begin at.
194
+ * @param {number} containerEnd The offset the block's contents end at.
195
+ * @return {Array} The nodes, with the whitespace between them spliced in.
196
+ */
197
+ function insertWhitespaceNodes (state, nodes, containerStart, containerEnd) {
198
+ if (!state.preserveFormatting) {
199
+ return nodes;
200
+ }
201
+ const result = [];
202
+ let cursor = containerStart;
203
+ for (const node of nodes) {
204
+ const startOffset = node.position?.start?.offset;
205
+ if (startOffset > cursor) {
206
+ result.push({
207
+ type: 'whitespace',
208
+ value: state.source.slice(cursor, startOffset)
209
+ });
210
+ }
211
+ result.push(node);
212
+ const endOffset = node.position?.end?.offset;
213
+ if (endOffset !== undefined) {
214
+ cursor = endOffset;
215
+ }
216
+ }
217
+ if (cursor < containerEnd) {
218
+ result.push({
219
+ type: 'whitespace',
220
+ value: state.source.slice(cursor, containerEnd)
221
+ });
222
+ }
223
+ return result;
224
+ }
225
+
226
+ /**
227
+ * Parses the comment at the current position.
228
+ *
229
+ * @param {object} state The state of the parse.
230
+ * @return {object|null} The comment node, or null when a comment is not what comes next.
231
+ */
232
+ function parseComment (state) {
233
+ const { scanner } = state;
234
+ const isComment = (
235
+ characterCodeAt(scanner) === CHARACTER_CODE.slash &&
236
+ characterCodeAt(scanner, 1) === CHARACTER_CODE.asterisk
237
+ );
238
+ if (!isComment) {
239
+ return null;
240
+ }
241
+ const startOffset = scanner.index;
242
+ const match = consumePattern(scanner, COMMENT_PATTERN);
243
+ const text = match[0];
244
+ if (!text.endsWith('*/')) {
245
+ recordError(state, 'End of comment missing');
246
+ return finishNode(state, { type: 'comment', comment: text.slice(2) }, startOffset);
247
+ }
248
+ return finishNode(state, { type: 'comment', comment: text.slice(2, -2) }, startOffset);
249
+ }
250
+
251
+ /**
252
+ * Parses the run of comments at the current position onto the end of a list
253
+ * of nodes.
254
+ *
255
+ * @param {object} state The state of the parse.
256
+ * @param {Array} nodes The list of nodes to append the comments to.
257
+ */
258
+ function parseComments (state, nodes) {
259
+ let comment = parseComment(state);
260
+ while (comment) {
261
+ nodes.push(comment);
262
+ comment = parseComment(state);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Parses the selectors of a rule, which are everything in front of the rule's
268
+ * block. Comments written between selectors describe the stylesheet rather
269
+ * than the elements it matches, so they are dropped.
270
+ *
271
+ * @param {object} state The state of the parse.
272
+ * @return {Array|null} The rule's selectors, or null when no block follows the current position.
273
+ */
274
+ function parseSelectorList (state) {
275
+ const { scanner } = state;
276
+ const braceIndex = findTopLevelDelimiter(state.source, '{', scanner.index);
277
+ if (braceIndex === -1 || braceIndex === scanner.index) {
278
+ return null;
279
+ }
280
+ const selectorText = consumeTo(scanner, braceIndex).trim().replace(COMMENT_SEARCH_PATTERN, '');
281
+ if (!selectorText) {
282
+ return [];
283
+ }
284
+ return splitTopLevel(selectorText, ',').map((selector) => {
285
+ return selector.trim();
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Determines whether a nested rule, rather than a declaration, is what comes
291
+ * next inside a block. A rule is what opens a block before it reaches the end
292
+ * of the declaration it would otherwise be.
293
+ *
294
+ * @param {object} state The state of the parse.
295
+ * @return {boolean} Whether a nested rule comes next.
296
+ */
297
+ function looksLikeNestedRule (state) {
298
+ const index = findTopLevelDelimiter(state.source, '{;}', state.scanner.index);
299
+ return index !== -1 && state.source[index] === '{';
300
+ }
301
+
302
+ /**
303
+ * Parses a single declaration, such as `color: red`.
304
+ *
305
+ * @param {object} state The state of the parse.
306
+ * @return {object|null} The declaration node, or null when a declaration is not what comes next.
307
+ */
308
+ function parseDeclaration (state) {
309
+ const {
310
+ scanner,
311
+ source
312
+ } = state;
313
+ const startOffset = scanner.index;
314
+ const propertyMatch = consumePattern(scanner, PROPERTY_NAME_PATTERN);
315
+ if (!propertyMatch) {
316
+ return null;
317
+ }
318
+ const property = propertyMatch[1].replace(COMMENT_SEARCH_PATTERN, '');
319
+ if (!consumeColon(scanner)) {
320
+ recordError(state, 'property missing \':\'');
321
+ return null;
322
+ }
323
+ const afterColonOffset = scanner.index;
324
+ let valueEndIndex = findTopLevelDelimiter(source, ';}', scanner.index);
325
+ if (valueEndIndex === -1) {
326
+ recordError(state, 'end of declaration missing');
327
+ valueEndIndex = source.length;
328
+ }
329
+ const rawValue = consumeTo(scanner, valueEndIndex);
330
+ const node = {
331
+ type: 'declaration',
332
+ property,
333
+ value: rawValue.trim().replace(COMMENT_SEARCH_PATTERN, '')
334
+ };
335
+ if (state.preserveFormatting) {
336
+ node.rawBetween = source.slice(startOffset + property.length, afterColonOffset);
337
+ node.rawValue = rawValue;
338
+ }
339
+ finishNode(state, node, startOffset);
340
+ skipSemicolonsAndWhitespace(scanner);
341
+ return node;
342
+ }
343
+
344
+ /**
345
+ * Steps over the text of something inside a block that could not be read,
346
+ * up to the end of the declaration it was written as, so that the rest of the
347
+ * block is still parsed. Only a silent parse recovers, and only when the
348
+ * block holds another declaration to recover to.
349
+ *
350
+ * @param {object} state The state of the parse.
351
+ * @return {boolean} Whether the parser recovered and can keep reading the block.
352
+ */
353
+ function recoverInsideBlock (state) {
354
+ const {
355
+ scanner,
356
+ source
357
+ } = state;
358
+ if (!state.silent || !hasMoreInput(scanner)) {
359
+ return false;
360
+ }
361
+ const semicolonIndex = source.indexOf(';', scanner.index);
362
+ if (semicolonIndex === -1) {
363
+ return false;
364
+ }
365
+ const closeBraceIndex = source.indexOf('}', scanner.index);
366
+ if (closeBraceIndex !== -1 && closeBraceIndex < semicolonIndex) {
367
+ return false;
368
+ }
369
+ consumeTo(scanner, semicolonIndex + 1);
370
+ skipWhitespace(scanner);
371
+ return true;
372
+ }
373
+
374
+ /**
375
+ * Parses whatever comes next inside a block, which may be a nested at-rule, a
376
+ * nested rule, or a declaration, depending on what the block is allowed to
377
+ * hold.
378
+ *
379
+ * @param {object} state The state of the parse.
380
+ * @param {object} contents What the block being parsed may hold.
381
+ * @return {object|null} The node that was parsed, or null when nothing could be.
382
+ */
383
+ function parseBlockItem (state, contents) {
384
+ const { scanner } = state;
385
+ if (contents.atRules && characterCodeAt(scanner) === CHARACTER_CODE.atSign) {
386
+ const atRule = parseAtRule(state);
387
+ if (atRule) {
388
+ return atRule;
389
+ }
390
+ }
391
+ if (contents.nestedRules && looksLikeNestedRule(state)) {
392
+ const nestedRule = parseRule(state);
393
+ if (nestedRule) {
394
+ return nestedRule;
395
+ }
396
+ }
397
+ return parseDeclaration(state);
398
+ }
399
+
400
+ /**
401
+ * Parses the contents of a block, up to but not including the brace that
402
+ * closes it.
403
+ *
404
+ * @param {object} state The state of the parse.
405
+ * @param {object} contents What the block being parsed may hold.
406
+ * @return {Array} The nodes of the block, in source order.
407
+ */
408
+ function parseBlockItems (state, contents) {
409
+ const { scanner } = state;
410
+ const items = [];
411
+ let parsing = true;
412
+ while (parsing) {
413
+ // A semicolon that follows nothing states an empty declaration, which
414
+ // declares as much as it says: nothing
415
+ skipSemicolonsAndWhitespace(scanner);
416
+ parseComments(state, items);
417
+ if (!hasMoreInput(scanner) || characterCodeAt(scanner) === CHARACTER_CODE.closeBrace) {
418
+ return items;
419
+ }
420
+ const item = parseBlockItem(state, contents);
421
+ if (item) {
422
+ items.push(item);
423
+ continue;
424
+ }
425
+ parsing = recoverInsideBlock(state);
426
+ }
427
+ return items;
428
+ }
429
+
430
+ /**
431
+ * Parses a whole block, from the brace that opens it to the brace that closes
432
+ * it.
433
+ *
434
+ * @param {object} state The state of the parse.
435
+ * @param {object} contents What the block being parsed may hold.
436
+ * @return {Array|null} The nodes of the block, or null when the block was not written as one.
437
+ */
438
+ function parseBlock (state, contents) {
439
+ const { scanner } = state;
440
+ const afterOpenOffset = scanner.index + 1;
441
+ if (!consumeOpenBrace(scanner)) {
442
+ recordError(state, 'missing \'{\'');
443
+ return null;
444
+ }
445
+ const items = parseBlockItems(state, contents);
446
+ const beforeCloseOffset = scanner.index;
447
+ if (!consumeCloseBrace(scanner)) {
448
+ recordError(state, 'missing \'}\'');
449
+ // The end of the stylesheet closes every block still open at it, so a
450
+ // stylesheet that stops mid-block still declares what it got through
451
+ if (hasMoreInput(scanner)) {
452
+ return null;
453
+ }
454
+ }
455
+ return insertWhitespaceNodes(state, items, afterOpenOffset, beforeCloseOffset);
456
+ }
457
+
458
+ /**
459
+ * Parses a rule, meaning its selectors and the block of declarations and
460
+ * nested rules they apply to.
461
+ *
462
+ * @param {object} state The state of the parse.
463
+ * @return {object|null} The rule node, or null when a rule is not what comes next.
464
+ */
465
+ function parseRule (state) {
466
+ const startOffset = state.scanner.index;
467
+ const selectors = parseSelectorList(state);
468
+ if (!selectors) {
469
+ recordError(state, 'selector missing');
470
+ return null;
471
+ }
472
+ const preludeEndOffset = state.scanner.index;
473
+ const node = {
474
+ type: 'rule',
475
+ selectors,
476
+ declarations: parseBlock(state, BLOCK_CONTENTS.rulesAndDeclarations) || []
477
+ };
478
+ addRawPrelude(state, node, startOffset, preludeEndOffset);
479
+ return finishNode(state, node, startOffset);
480
+ }
481
+
482
+ /**
483
+ * Parses one stop of an animation, such as the `from {}` of a `@keyframes`
484
+ * rule.
485
+ *
486
+ * @param {object} state The state of the parse.
487
+ * @return {object|null} The keyframe node, or null when a keyframe is not what comes next.
488
+ */
489
+ function parseKeyframe (state) {
490
+ const { scanner } = state;
491
+ const startOffset = scanner.index;
492
+ const values = [];
493
+ let stopMatch = consumePattern(scanner, KEYFRAME_STOP_PATTERN);
494
+ while (stopMatch) {
495
+ values.push(stopMatch[1]);
496
+ consumeComma(scanner);
497
+ stopMatch = consumePattern(scanner, KEYFRAME_STOP_PATTERN);
498
+ }
499
+ if (!values.length) {
500
+ return null;
501
+ }
502
+ const preludeEndOffset = scanner.index;
503
+ const declarations = parseBlock(state, BLOCK_CONTENTS.declarationsOnly);
504
+ if (!declarations) {
505
+ return null;
506
+ }
507
+ const node = {
508
+ type: 'keyframe',
509
+ values,
510
+ declarations
511
+ };
512
+ addRawPrelude(state, node, startOffset, preludeEndOffset);
513
+ return finishNode(state, node, startOffset);
514
+ }
515
+
516
+ /**
517
+ * Parses the block of a `@keyframes` rule, which holds the stops of the
518
+ * animation it describes.
519
+ *
520
+ * @param {object} state The state of the parse.
521
+ * @return {Array|null} The keyframe nodes, or null when the block was not written as one.
522
+ */
523
+ function parseKeyframesBlock (state) {
524
+ const { scanner } = state;
525
+ const afterOpenOffset = scanner.index + 1;
526
+ if (!consumeOpenBrace(scanner)) {
527
+ recordError(state, '@keyframes missing \'{\'');
528
+ return null;
529
+ }
530
+ const frames = [];
531
+ parseComments(state, frames);
532
+ let frame = parseKeyframe(state);
533
+ while (frame) {
534
+ frames.push(frame);
535
+ parseComments(state, frames);
536
+ frame = parseKeyframe(state);
537
+ }
538
+ const beforeCloseOffset = scanner.index;
539
+ if (!consumeCloseBrace(scanner)) {
540
+ recordError(state, '@keyframes missing \'}\'');
541
+ return null;
542
+ }
543
+ return insertWhitespaceNodes(state, frames, afterOpenOffset, beforeCloseOffset);
544
+ }
545
+
546
+ /**
547
+ * Reads the fields an at-rule's prelude declares, such as the condition of a
548
+ * `@media` rule or the name of a `@property` rule.
549
+ *
550
+ * @param {object} definition How the at-rule is written.
551
+ * @param {Array} match The match of the at-rule's prelude pattern.
552
+ * @return {object} The fields the prelude declares.
553
+ */
554
+ function readPreludeFields (definition, match) {
555
+ const fields = {};
556
+ definition.preludeFields.forEach((fieldName, fieldIndex) => {
557
+ fields[fieldName] = (match[fieldIndex + 1] || '').trim();
558
+ });
559
+ return fields;
560
+ }
561
+
562
+ /**
563
+ * Parses an at-rule that has no block and ends at a semicolon, such as an
564
+ * `@import`. What the statement declares is kept as raw text, because an
565
+ * engine reads some of these, `@charset` above all, as bytes rather than as
566
+ * CSS.
567
+ *
568
+ * @param {object} state The state of the parse.
569
+ * @param {object} definition How the at-rule is written.
570
+ * @param {number} startOffset The offset the at-rule began at.
571
+ * @return {object|null} The at-rule node, or null when the at-rule was not written as a statement.
572
+ */
573
+ function parseStatementAtRule (state, definition, startOffset) {
574
+ const {
575
+ scanner,
576
+ source
577
+ } = state;
578
+ const terminatorIndex = findTopLevelDelimiter(source, ';{}', scanner.index);
579
+ if (terminatorIndex !== -1 && source[terminatorIndex] !== ';') {
580
+ return null;
581
+ }
582
+ const preludeEnd = terminatorIndex === -1 ? source.length : terminatorIndex;
583
+ const preludeParts = splitStatementPrelude(source.slice(scanner.index, preludeEnd), definition);
584
+ if (!preludeParts) {
585
+ return null;
586
+ }
587
+ consumeTo(scanner, terminatorIndex === -1 ? source.length : terminatorIndex + 1);
588
+ const node = {
589
+ type: definition.type,
590
+ ...preludeParts
591
+ };
592
+ if (state.preserveFormatting) {
593
+ node.rawSource = source.slice(startOffset, scanner.index);
594
+ }
595
+ return finishNode(state, node, startOffset);
596
+ }
597
+
598
+ /**
599
+ * Splits what a statement at-rule declares into the fields of its node. Most
600
+ * statements declare a single thing, while `@custom-media` names the query it
601
+ * declares before the query itself.
602
+ *
603
+ * @param {string} preludeText The raw text between the at-rule's name and its semicolon.
604
+ * @param {object} definition How the at-rule is written.
605
+ * @return {object|null} The fields of the node, or null when the statement is missing one.
606
+ */
607
+ function splitStatementPrelude (preludeText, definition) {
608
+ const trimmedPrelude = preludeText.trim();
609
+ if (!trimmedPrelude) {
610
+ return null;
611
+ }
612
+ if (definition.preludeFields.length === 1) {
613
+ return { [definition.preludeFields[0]]: trimmedPrelude };
614
+ }
615
+ // The name of the query ends at the first whitespace, and the query itself is the rest
616
+ const separatorIndex = trimmedPrelude.search(/\s/);
617
+ if (separatorIndex === -1) {
618
+ return null;
619
+ }
620
+ const [nameField, conditionField] = definition.preludeFields;
621
+ return {
622
+ [nameField]: trimmedPrelude.slice(0, separatorIndex),
623
+ [conditionField]: trimmedPrelude.slice(separatorIndex + 1).trim()
624
+ };
625
+ }
626
+
627
+ /**
628
+ * Parses the body of an at-rule onto its node, which is the last thing an
629
+ * at-rule is made of and the only part that differs between the kinds of
630
+ * at-rule that have one.
631
+ *
632
+ * @param {object} state The state of the parse.
633
+ * @param {object} definition How the at-rule is written.
634
+ * @param {object} node The node the body belongs to.
635
+ * @return {boolean} Whether the body was read.
636
+ */
637
+ function parseAtRuleBody (state, definition, node) {
638
+ if (definition.body === AT_RULE_BODY.keyframes) {
639
+ node.keyframes = parseKeyframesBlock(state);
640
+ return Boolean(node.keyframes);
641
+ }
642
+ if (definition.body === AT_RULE_BODY.declarations) {
643
+ node.declarations = parseBlock(state, BLOCK_CONTENTS.declarationsOnly);
644
+ return Boolean(node.declarations);
645
+ }
646
+ if (definition.body === AT_RULE_BODY.page) {
647
+ node.selectors = parseSelectorList(state) || [];
648
+ node.declarations = parseBlock(state, BLOCK_CONTENTS.pageBody);
649
+ return Boolean(node.declarations);
650
+ }
651
+ node.rules = parseBlock(state, BLOCK_CONTENTS.rulesAndDeclarations);
652
+ return Boolean(node.rules);
653
+ }
654
+
655
+ /**
656
+ * Parses an at-rule the parser knows by name, such as `@media`. An at-rule
657
+ * that is not written the way its name says it should be is not parsed here,
658
+ * so that it can be read as a generic at-rule instead.
659
+ *
660
+ * @param {object} state The state of the parse.
661
+ * @param {object} definition How the at-rule is written.
662
+ * @param {number} startOffset The offset the at-rule began at.
663
+ * @return {object|null} The at-rule node, or null when the at-rule is written some other way.
664
+ */
665
+ function parseDefinedAtRule (state, definition, startOffset) {
666
+ const { scanner } = state;
667
+ if (definition.body === AT_RULE_BODY.statement) {
668
+ return parseStatementAtRule(state, definition, startOffset);
669
+ }
670
+ const preludeMatch = consumePattern(scanner, definition.preludePattern);
671
+ if (!preludeMatch) {
672
+ return null;
673
+ }
674
+ const preludeEndOffset = scanner.index;
675
+ const node = {
676
+ type: definition.type,
677
+ ...readPreludeFields(definition, preludeMatch)
678
+ };
679
+ if (definition.name) {
680
+ node.name = definition.name;
681
+ }
682
+ if (definition.vendor !== undefined) {
683
+ node.vendor = definition.vendor;
684
+ }
685
+ const isStatementForm = (
686
+ definition.statementFallback &&
687
+ characterCodeAt(scanner) !== CHARACTER_CODE.openBrace
688
+ );
689
+ if (isStatementForm) {
690
+ skipSemicolonsAndWhitespace(scanner);
691
+ if (state.preserveFormatting) {
692
+ node.rawSource = state.source.slice(startOffset, scanner.index);
693
+ }
694
+ return finishNode(state, node, startOffset);
695
+ }
696
+ if (!parseAtRuleBody(state, definition, node)) {
697
+ return null;
698
+ }
699
+ addRawPrelude(state, node, startOffset, preludeEndOffset);
700
+ return finishNode(state, node, startOffset);
701
+ }
702
+
703
+ /**
704
+ * Parses an at-rule the parser does not know, keeping whatever it declares as
705
+ * raw text so that a stylesheet is never emptied of the at-rules that were
706
+ * added to CSS after this parser was written.
707
+ *
708
+ * @param {object} state The state of the parse.
709
+ * @param {string} name The at-rule's name, without its at-sign.
710
+ * @param {number} startOffset The offset the at-rule began at.
711
+ * @return {object} The at-rule node.
712
+ */
713
+ function parseGenericAtRule (state, name, startOffset) {
714
+ const {
715
+ scanner,
716
+ source
717
+ } = state;
718
+ scanner.index = startOffset + 1 + name.length;
719
+ skipWhitespace(scanner);
720
+ const preludeEnd = findTopLevelDelimiter(source, '{;', scanner.index);
721
+ let prelude = '';
722
+ if (preludeEnd > scanner.index) {
723
+ prelude = consumeTo(scanner, preludeEnd).trim();
724
+ }
725
+ const preludeEndOffset = scanner.index;
726
+ const node = {
727
+ type: 'at-rule',
728
+ name,
729
+ prelude
730
+ };
731
+ if (characterCodeAt(scanner) !== CHARACTER_CODE.openBrace) {
732
+ skipSemicolonsAndWhitespace(scanner);
733
+ return finishNode(state, node, startOffset);
734
+ }
735
+ node.rules = parseBlock(state, BLOCK_CONTENTS.rulesAndDeclarations) || [];
736
+ addRawPrelude(state, node, startOffset, preludeEndOffset);
737
+ return finishNode(state, node, startOffset);
738
+ }
739
+
740
+ /**
741
+ * Parses the at-rule at the current position.
742
+ *
743
+ * @param {object} state The state of the parse.
744
+ * @return {object|null} The at-rule node, or null when an at-rule is not what comes next.
745
+ */
746
+ function parseAtRule (state) {
747
+ const { scanner } = state;
748
+ if (characterCodeAt(scanner) !== CHARACTER_CODE.atSign) {
749
+ return null;
750
+ }
751
+ const nameMatch = peekPattern(scanner, AT_RULE_NAME_PATTERN);
752
+ if (!nameMatch) {
753
+ return null;
754
+ }
755
+ const name = nameMatch[1];
756
+ const startOffset = scanner.index;
757
+ const definition = findAtRuleDefinition(name);
758
+ if (definition) {
759
+ scanner.index += nameMatch[0].length;
760
+ const node = parseDefinedAtRule(state, definition, startOffset);
761
+ if (node) {
762
+ return node;
763
+ }
764
+ scanner.index = startOffset;
765
+ }
766
+ return parseGenericAtRule(state, name, startOffset);
767
+ }
768
+
769
+ /**
770
+ * Parses the rules of a stylesheet, which are the at-rules, rules, and
771
+ * comments written at its top level.
772
+ *
773
+ * @param {object} state The state of the parse.
774
+ * @return {Array} The top-level nodes of the stylesheet, in source order.
775
+ */
776
+ function parseRuleList (state) {
777
+ const { scanner } = state;
778
+ const rules = [];
779
+ skipWhitespace(scanner);
780
+ parseComments(state, rules);
781
+ while (hasMoreInput(scanner)) {
782
+ if (characterCodeAt(scanner) === CHARACTER_CODE.closeBrace) {
783
+ recordError(state, 'extra \'}\'');
784
+ scanner.index++;
785
+ } else {
786
+ const node = parseAtRule(state) || parseRule(state);
787
+ if (node) {
788
+ rules.push(node);
789
+ } else {
790
+ if (!state.silent) {
791
+ return rules;
792
+ }
793
+ scanner.index++;
794
+ }
795
+ }
796
+ skipWhitespace(scanner);
797
+ parseComments(state, rules);
798
+ }
799
+ return rules;
800
+ }
801
+
802
+ /**
803
+ * Parses a string of CSS into an abstract syntax tree.
804
+ *
805
+ * @param {string} input Any valid CSS input
806
+ * @param {object} options How to parse: `source` names the file, `silent` collects errors instead of throwing them, and `preserveFormatting` keeps the whitespace and raw text the CSS was written with.
807
+ * @return {object} An object representation of the syntax
808
+ */
809
+ export const parse = function (input, options = {}) {
810
+ let source;
811
+ if (typeof input === 'string') {
812
+ source = input;
813
+ } else {
814
+ source = String(input ?? '');
815
+ }
816
+ const state = createParserState(source, options);
817
+ const rules = parseRuleList(state);
818
+ return {
819
+ type: 'stylesheet',
820
+ stylesheet: {
821
+ source: state.sourceName,
822
+ rules: insertWhitespaceNodes(state, rules, 0, source.length),
823
+ parsingErrors: state.parsingErrors
824
+ }
825
+ };
826
+ };