@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.
- package/README.md +7 -2
- package/package.json +7 -6
- package/src/declarations/background.js +29 -37
- package/src/declarations/border.js +1 -1
- package/src/declarations/fold.js +1 -1
- package/src/declarations/process.js +17 -8
- package/src/declarations/shorthand-values.js +1 -1
- package/src/index.js +15 -44
- package/src/parser/at-rules.js +291 -0
- package/src/parser/index.js +826 -0
- package/src/parser/positions.js +71 -0
- package/src/parser/scanner.js +229 -0
- package/src/parser/source-search.js +287 -0
- package/src/parser/tokenizer.js +396 -0
- package/src/preprocess.js +20 -228
- package/src/rules/lang.js +20 -56
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +183 -110
- package/src/rules/selectors.js +56 -221
- package/src/rules/stringify.js +4 -5
- package/src/value/color-mix.js +22 -47
- package/src/value/gradients.js +44 -71
- package/src/value/light-dark.js +9 -94
- package/src/value/math.js +3 -23
- package/src/value/minify.js +84 -51
- package/src/value/relative-colors.js +15 -78
- package/src/value/shared.js +1 -0
- package/src/value/tokens.js +4 -387
- package/src/value/transforms.js +20 -53
- package/src/value/syntax.js +0 -144
|
@@ -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
|
+
};
|