@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,71 @@
1
+ /**
2
+ * @file Turns offsets into the stylesheet into the line and column numbers that AST nodes report.
3
+ */
4
+
5
+ /**
6
+ * Records the offset every line of the source begins at, so that any offset
7
+ * can later be resolved without rescanning the text in front of it.
8
+ *
9
+ * @param {string} source The raw CSS text.
10
+ * @return {Array} The offset each line starts at, in order.
11
+ */
12
+ function createLineIndex (source) {
13
+ const lineStartOffsets = [0];
14
+ let lineBreakIndex = source.indexOf('\n');
15
+ while (lineBreakIndex !== -1) {
16
+ lineStartOffsets.push(lineBreakIndex + 1);
17
+ lineBreakIndex = source.indexOf('\n', lineBreakIndex + 1);
18
+ }
19
+ return lineStartOffsets;
20
+ }
21
+
22
+ /**
23
+ * Resolves an offset into the stylesheet to the one-based line and column it
24
+ * falls on, by binary searching the line index for the last line that begins
25
+ * at or before the offset.
26
+ *
27
+ * @param {Array} lineStartOffsets The offset each line starts at.
28
+ * @param {number} offset The offset to resolve.
29
+ * @return {object} The line, column, and offset of that point in the source.
30
+ */
31
+ function resolveSourceLocation (lineStartOffsets, offset) {
32
+ let lowestLine = 0;
33
+ let highestLine = lineStartOffsets.length - 1;
34
+ while (lowestLine < highestLine) {
35
+ const middleLine = Math.ceil((lowestLine + highestLine) / 2);
36
+ if (lineStartOffsets[middleLine] <= offset) {
37
+ lowestLine = middleLine;
38
+ } else {
39
+ highestLine = middleLine - 1;
40
+ }
41
+ }
42
+ return {
43
+ line: lowestLine + 1,
44
+ column: offset - lineStartOffsets[lowestLine] + 1,
45
+ offset
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Builds the position an AST node reports, spanning the text the node was
51
+ * parsed from.
52
+ *
53
+ * @param {Array} lineStartOffsets The offset each line starts at.
54
+ * @param {number} startOffset The offset the node begins at.
55
+ * @param {number} endOffset The offset just past the end of the node.
56
+ * @param {string} sourceName The name of the file the CSS came from.
57
+ * @return {object} The position of the node in the source.
58
+ */
59
+ function createPosition (lineStartOffsets, startOffset, endOffset, sourceName) {
60
+ return {
61
+ start: resolveSourceLocation(lineStartOffsets, startOffset),
62
+ end: resolveSourceLocation(lineStartOffsets, endOffset),
63
+ source: sourceName
64
+ };
65
+ }
66
+
67
+ export {
68
+ createLineIndex,
69
+ createPosition,
70
+ resolveSourceLocation
71
+ };
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @file Reads a CSS source string one token at a time, keeping track of how far into the text the parser has gotten.
3
+ */
4
+
5
+ /**
6
+ * The character codes the parser compares against while scanning. Comparing
7
+ * numeric codes avoids building a one-character string for every character of
8
+ * the stylesheet.
9
+ */
10
+ const CHARACTER_CODE = {
11
+ tab: 9,
12
+ lineFeed: 10,
13
+ formFeed: 12,
14
+ carriageReturn: 13,
15
+ space: 32,
16
+ asterisk: 42,
17
+ comma: 44,
18
+ slash: 47,
19
+ colon: 58,
20
+ semicolon: 59,
21
+ atSign: 64,
22
+ openBrace: 123,
23
+ closeBrace: 125
24
+ };
25
+
26
+ /**
27
+ * Determines whether a character code is one of the five characters CSS counts
28
+ * as whitespace.
29
+ *
30
+ * @param {number} characterCode The character code to test.
31
+ * @return {boolean} Whether the character is CSS whitespace.
32
+ */
33
+ function isWhitespaceCode (characterCode) {
34
+ return (
35
+ characterCode === CHARACTER_CODE.space ||
36
+ characterCode === CHARACTER_CODE.lineFeed ||
37
+ characterCode === CHARACTER_CODE.tab ||
38
+ characterCode === CHARACTER_CODE.carriageReturn ||
39
+ characterCode === CHARACTER_CODE.formFeed
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Creates the reader the parser advances through the stylesheet with.
45
+ *
46
+ * @param {string} source The raw CSS text to read.
47
+ * @return {object} The scanner, positioned at the start of the text.
48
+ */
49
+ function createScanner (source) {
50
+ return {
51
+ source,
52
+ index: 0
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Determines whether the scanner has any text left to read.
58
+ *
59
+ * @param {object} scanner The scanner to check.
60
+ * @return {boolean} Whether text remains.
61
+ */
62
+ function hasMoreInput (scanner) {
63
+ return scanner.index < scanner.source.length;
64
+ }
65
+
66
+ /**
67
+ * Reads the character code at an offset from the scanner's position without
68
+ * advancing it. Reading past the end of the text reports `NaN`, which fails
69
+ * every comparison, so the end of input needs no separate check.
70
+ *
71
+ * @param {object} scanner The scanner to read from.
72
+ * @param {number} offset How far ahead of the current position to read.
73
+ * @return {number} The character code at that position.
74
+ */
75
+ function characterCodeAt (scanner, offset = 0) {
76
+ return scanner.source.charCodeAt(scanner.index + offset);
77
+ }
78
+
79
+ /**
80
+ * Advances the scanner past any run of whitespace.
81
+ *
82
+ * @param {object} scanner The scanner to advance.
83
+ */
84
+ function skipWhitespace (scanner) {
85
+ while (isWhitespaceCode(scanner.source.charCodeAt(scanner.index))) {
86
+ scanner.index++;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Advances the scanner past the semicolons and whitespace that separate a
92
+ * declaration from whatever follows it.
93
+ *
94
+ * @param {object} scanner The scanner to advance.
95
+ */
96
+ function skipSemicolonsAndWhitespace (scanner) {
97
+ let characterCode = scanner.source.charCodeAt(scanner.index);
98
+ while (isWhitespaceCode(characterCode) || characterCode === CHARACTER_CODE.semicolon) {
99
+ scanner.index++;
100
+ characterCode = scanner.source.charCodeAt(scanner.index);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Advances the scanner to an absolute index, returning the text that was
106
+ * passed over.
107
+ *
108
+ * @param {object} scanner The scanner to advance.
109
+ * @param {number} endIndex The index to stop at.
110
+ * @return {string} The text between the old and the new position.
111
+ */
112
+ function consumeTo (scanner, endIndex) {
113
+ const text = scanner.source.slice(scanner.index, endIndex);
114
+ scanner.index = endIndex;
115
+ return text;
116
+ }
117
+
118
+ /**
119
+ * Applies a sticky pattern at the scanner's position without advancing it, so
120
+ * the parser can decide what it is looking at before committing to reading it.
121
+ *
122
+ * @param {object} scanner The scanner to read from.
123
+ * @param {RegExp} pattern A sticky pattern to apply at the current position.
124
+ * @return {Array<string>|null} The match, or null when the pattern does not apply.
125
+ */
126
+ function peekPattern (scanner, pattern) {
127
+ pattern.lastIndex = scanner.index;
128
+ return pattern.exec(scanner.source);
129
+ }
130
+
131
+ /**
132
+ * Applies a sticky pattern at the scanner's position, advancing past whatever
133
+ * it matched.
134
+ *
135
+ * @param {object} scanner The scanner to read from.
136
+ * @param {RegExp} pattern A sticky pattern to apply at the current position.
137
+ * @return {Array<string>|null} The match, or null when the pattern does not apply.
138
+ */
139
+ function consumePattern (scanner, pattern) {
140
+ const match = peekPattern(scanner, pattern);
141
+ if (match) {
142
+ scanner.index += match[0].length;
143
+ }
144
+ return match;
145
+ }
146
+
147
+ /**
148
+ * Consumes a single expected character, reporting whether it was there.
149
+ *
150
+ * @param {object} scanner The scanner to advance.
151
+ * @param {number} characterCode The character code that is expected.
152
+ * @return {boolean} Whether the character was found and consumed.
153
+ */
154
+ function consumeCharacter (scanner, characterCode) {
155
+ if (scanner.source.charCodeAt(scanner.index) !== characterCode) {
156
+ return false;
157
+ }
158
+ scanner.index++;
159
+ return true;
160
+ }
161
+
162
+ /**
163
+ * Consumes an opening brace along with the whitespace that follows it.
164
+ *
165
+ * @param {object} scanner The scanner to advance.
166
+ * @return {boolean} Whether a block was opened.
167
+ */
168
+ function consumeOpenBrace (scanner) {
169
+ if (!consumeCharacter(scanner, CHARACTER_CODE.openBrace)) {
170
+ return false;
171
+ }
172
+ skipWhitespace(scanner);
173
+ return true;
174
+ }
175
+
176
+ /**
177
+ * Consumes a closing brace.
178
+ *
179
+ * @param {object} scanner The scanner to advance.
180
+ * @return {boolean} Whether a block was closed.
181
+ */
182
+ function consumeCloseBrace (scanner) {
183
+ return consumeCharacter(scanner, CHARACTER_CODE.closeBrace);
184
+ }
185
+
186
+ /**
187
+ * Consumes a colon along with the whitespace that follows it.
188
+ *
189
+ * @param {object} scanner The scanner to advance.
190
+ * @return {boolean} Whether a colon was found and consumed.
191
+ */
192
+ function consumeColon (scanner) {
193
+ if (!consumeCharacter(scanner, CHARACTER_CODE.colon)) {
194
+ return false;
195
+ }
196
+ skipWhitespace(scanner);
197
+ return true;
198
+ }
199
+
200
+ /**
201
+ * Consumes a comma along with the whitespace that follows it.
202
+ *
203
+ * @param {object} scanner The scanner to advance.
204
+ * @return {boolean} Whether a comma was found and consumed.
205
+ */
206
+ function consumeComma (scanner) {
207
+ if (!consumeCharacter(scanner, CHARACTER_CODE.comma)) {
208
+ return false;
209
+ }
210
+ skipWhitespace(scanner);
211
+ return true;
212
+ }
213
+
214
+ export {
215
+ CHARACTER_CODE,
216
+ characterCodeAt,
217
+ consumeCharacter,
218
+ consumeCloseBrace,
219
+ consumeColon,
220
+ consumeComma,
221
+ consumeOpenBrace,
222
+ consumePattern,
223
+ consumeTo,
224
+ createScanner,
225
+ hasMoreInput,
226
+ peekPattern,
227
+ skipSemicolonsAndWhitespace,
228
+ skipWhitespace
229
+ };
@@ -0,0 +1,287 @@
1
+ /**
2
+ * @file Locates the delimiters, groups, and tokens of raw CSS text, ignoring the delimiters that sit inside strings, escapes, or the parentheses and brackets of a group.
3
+ */
4
+
5
+ /**
6
+ * The characters that separate the top-level components of a CSS value: the
7
+ * five characters CSS counts as whitespace, and the hash that always begins a
8
+ * token of its own.
9
+ *
10
+ * @type {string}
11
+ */
12
+ const COMPONENT_SEPARATORS = ' \t\n\r\f#';
13
+
14
+ /**
15
+ * Determines whether a character opens a quoted string.
16
+ *
17
+ * @param {string} character The character to test.
18
+ * @return {boolean} Whether a quoted string starts here.
19
+ */
20
+ function isQuote (character) {
21
+ return character === '"' || character === '\'';
22
+ }
23
+
24
+ /**
25
+ * Reads past a quoted string, starting at its opening quote, so that the
26
+ * delimiters written inside the string are never mistaken for CSS syntax.
27
+ * An unterminated string runs to the end of the text.
28
+ *
29
+ * @param {string} source The raw CSS text being scanned.
30
+ * @param {number} openIndex The index of the opening quote character.
31
+ * @return {number} The index just past the closing quote.
32
+ */
33
+ function skipQuotedString (source, openIndex) {
34
+ const quoteCharacter = source[openIndex];
35
+ let index = openIndex + 1;
36
+ while (index < source.length) {
37
+ const character = source[index];
38
+ if (character === '\\') {
39
+ index += 2;
40
+ continue;
41
+ }
42
+ if (character === quoteCharacter) {
43
+ return index + 1;
44
+ }
45
+ index++;
46
+ }
47
+ return index;
48
+ }
49
+
50
+ /**
51
+ * Finds the character that closes the group opened at the given index, so that
52
+ * the delimiters written inside a group such as `url(a;b)` stay part of it.
53
+ * Strings, escapes, and groups of the same kind nested within it are read past
54
+ * on the way.
55
+ *
56
+ * @param {string} source The raw CSS text being scanned.
57
+ * @param {number} openIndex The index of the character that opens the group.
58
+ * @param {string} closeCharacter The character that closes the group.
59
+ * @return {number} The index of the closing character, or -1 when the group never closes.
60
+ */
61
+ function findGroupClose (source, openIndex, closeCharacter) {
62
+ const openCharacter = source[openIndex];
63
+ let depth = 1;
64
+ let index = openIndex + 1;
65
+ while (index < source.length) {
66
+ const character = source[index];
67
+ if (character === '\\') {
68
+ index += 2;
69
+ continue;
70
+ }
71
+ if (isQuote(character)) {
72
+ index = skipQuotedString(source, index);
73
+ continue;
74
+ }
75
+ if (character === openCharacter) {
76
+ depth++;
77
+ } else if (character === closeCharacter) {
78
+ depth--;
79
+ if (!depth) {
80
+ return index;
81
+ }
82
+ }
83
+ index++;
84
+ }
85
+ return -1;
86
+ }
87
+
88
+ /**
89
+ * Finds the parenthesis closing the one at the given index.
90
+ *
91
+ * @param {string} source The raw CSS text being scanned.
92
+ * @param {number} openIndex The index of the opening parenthesis.
93
+ * @return {number} The index of the matching closing parenthesis, or -1 when it never closes.
94
+ */
95
+ function findMatchingParenthesis (source, openIndex) {
96
+ return findGroupClose(source, openIndex, ')');
97
+ }
98
+
99
+ /**
100
+ * Finds the bracket closing the one at the given index.
101
+ *
102
+ * @param {string} source The raw CSS text being scanned.
103
+ * @param {number} openIndex The index of the opening bracket.
104
+ * @return {number} The index of the matching closing bracket, or -1 when it never closes.
105
+ */
106
+ function findMatchingBracket (source, openIndex) {
107
+ return findGroupClose(source, openIndex, ']');
108
+ }
109
+
110
+ /**
111
+ * Reads past a group, starting at the character that opens it. A group that
112
+ * never closes runs to the end of the text.
113
+ *
114
+ * @param {string} source The raw CSS text being scanned.
115
+ * @param {number} openIndex The index of the character that opens the group.
116
+ * @param {string} closeCharacter The character that closes the group.
117
+ * @return {number} The index just past the end of the group.
118
+ */
119
+ function skipGroup (source, openIndex, closeCharacter) {
120
+ const closeIndex = findGroupClose(source, openIndex, closeCharacter);
121
+ if (closeIndex === -1) {
122
+ return source.length;
123
+ }
124
+ return closeIndex + 1;
125
+ }
126
+
127
+ /**
128
+ * Reads past a parenthesized group, such as the arguments of a function.
129
+ *
130
+ * @param {string} source The raw CSS text being scanned.
131
+ * @param {number} openIndex The index of the opening parenthesis.
132
+ * @return {number} The index just past the matching closing parenthesis.
133
+ */
134
+ function skipParenthesizedGroup (source, openIndex) {
135
+ return skipGroup(source, openIndex, ')');
136
+ }
137
+
138
+ /**
139
+ * Reads past a bracketed group, which in a selector is an attribute selector
140
+ * and in a value is a set of grid line names.
141
+ *
142
+ * @param {string} source The raw CSS text being scanned.
143
+ * @param {number} openIndex The index of the opening bracket.
144
+ * @return {number} The index just past the matching closing bracket.
145
+ */
146
+ function skipBracketedGroup (source, openIndex) {
147
+ return skipGroup(source, openIndex, ']');
148
+ }
149
+
150
+ /**
151
+ * Determines whether a `url()` token begins at the given index. The contents
152
+ * of one are a URL rather than CSS syntax, so a pass that rewrites a value
153
+ * reads past a `url()` instead of into it.
154
+ *
155
+ * @param {string} source The raw CSS text being scanned.
156
+ * @param {number} index The index to test.
157
+ * @return {boolean} Whether a `url()` token starts here.
158
+ */
159
+ function startsUrlToken (source, index) {
160
+ return source.slice(index, index + 4).toLowerCase() === 'url(';
161
+ }
162
+
163
+ /**
164
+ * Finds the first of several delimiter characters that is written at the top
165
+ * level of the text, meaning it is not escaped, quoted, or nested inside a
166
+ * group.
167
+ *
168
+ * @param {string} source The raw CSS text being scanned.
169
+ * @param {string} delimiters The delimiter characters to look for.
170
+ * @param {number} startIndex The index to start scanning from.
171
+ * @return {number} The index of the first top-level delimiter, or -1 when there is none.
172
+ */
173
+ function findTopLevelDelimiter (source, delimiters, startIndex) {
174
+ let index = startIndex;
175
+ while (index < source.length) {
176
+ const character = source[index];
177
+ if (character === '\\') {
178
+ index += 2;
179
+ continue;
180
+ }
181
+ if (isQuote(character)) {
182
+ index = skipQuotedString(source, index);
183
+ continue;
184
+ }
185
+ if (character === '(') {
186
+ index = skipParenthesizedGroup(source, index);
187
+ continue;
188
+ }
189
+ if (character === '[') {
190
+ index = skipBracketedGroup(source, index);
191
+ continue;
192
+ }
193
+ if (delimiters.includes(character)) {
194
+ return index;
195
+ }
196
+ index++;
197
+ }
198
+ return -1;
199
+ }
200
+
201
+ /**
202
+ * Splits text on every top-level occurrence of any of the delimiters, keeping
203
+ * the delimiters that are escaped, quoted, or nested inside a group within
204
+ * their part.
205
+ *
206
+ * @param {string} text The text to split.
207
+ * @param {string} delimiters The delimiter characters to split on.
208
+ * @return {Array} The parts of the text, without the delimiters.
209
+ */
210
+ function splitTopLevel (text, delimiters) {
211
+ const parts = [];
212
+ let partStart = 0;
213
+ let index = findTopLevelDelimiter(text, delimiters, 0);
214
+ while (index !== -1) {
215
+ parts.push(text.slice(partStart, index));
216
+ partStart = index + 1;
217
+ index = findTopLevelDelimiter(text, delimiters, partStart);
218
+ }
219
+ parts.push(text.slice(partStart));
220
+ return parts;
221
+ }
222
+
223
+ /**
224
+ * Splits a comma-separated list, such as the arguments of a function or the
225
+ * selectors of a rule, into its trimmed parts. A list with nothing in it has
226
+ * no parts at all.
227
+ *
228
+ * @param {string} text The comma-separated text to split.
229
+ * @return {Array} The parts of the list, trimmed of the whitespace written around them.
230
+ */
231
+ function splitTopLevelCommaList (text) {
232
+ if (!text.trim()) {
233
+ return [];
234
+ }
235
+ return splitTopLevel(text, ',').map((part) => {
236
+ return part.trim();
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Splits a CSS value into its top-level components, keeping the arguments of a
242
+ * function and the contents of a quoted string intact. Components are
243
+ * separated by whitespace, or by a `#`, which always starts a hash token and
244
+ * therefore ends any component already in progress.
245
+ *
246
+ * For example, `rgb(0 0 0) red` yields `["rgb(0 0 0)", "red"]`, and the
247
+ * minified `red#00f` yields `["red", "#00f"]`.
248
+ *
249
+ * @param {string} value The CSS value to split.
250
+ * @return {Array} The top-level components of the value.
251
+ */
252
+ function splitTopLevelComponents (value) {
253
+ const components = [];
254
+ let componentStart = 0;
255
+ let separatorIndex = findTopLevelDelimiter(value, COMPONENT_SEPARATORS, 0);
256
+ while (separatorIndex !== -1) {
257
+ const component = value.slice(componentStart, separatorIndex);
258
+ if (component) {
259
+ components.push(component);
260
+ }
261
+ // A hash belongs to the token it starts, so it is not consumed as a separator
262
+ if (value[separatorIndex] === '#') {
263
+ componentStart = separatorIndex;
264
+ } else {
265
+ componentStart = separatorIndex + 1;
266
+ }
267
+ separatorIndex = findTopLevelDelimiter(value, COMPONENT_SEPARATORS, separatorIndex + 1);
268
+ }
269
+ const lastComponent = value.slice(componentStart);
270
+ if (lastComponent) {
271
+ components.push(lastComponent);
272
+ }
273
+ return components;
274
+ }
275
+
276
+ export {
277
+ findMatchingBracket,
278
+ findMatchingParenthesis,
279
+ findTopLevelDelimiter,
280
+ skipBracketedGroup,
281
+ skipParenthesizedGroup,
282
+ skipQuotedString,
283
+ splitTopLevel,
284
+ splitTopLevelCommaList,
285
+ splitTopLevelComponents,
286
+ startsUrlToken
287
+ };