@excom/quark-parser 0.1.0
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/.rush/temp/chunked-rush-logs/quark-parser.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark-parser.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +5 -0
- package/index.ts +12 -0
- package/package.json +39 -0
- package/rush-logs/quark-parser.apply-exports.cache.log +1 -0
- package/rush-logs/quark-parser.apply-exports.log +1 -0
- package/rush-logs/quark-parser.build_package-metas.cache.log +1 -0
- package/rush-logs/quark-parser.build_package-metas.log +1 -0
- package/src/error.ts +24 -0
- package/src/parser.ts +1482 -0
- package/src/tables.ts +77 -0
- package/src/tokenizer.ts +443 -0
- package/src/types.ts +497 -0
- package/support/docs/README.md +443 -0
- package/support/package-meta.json +33 -0
- package/support/tests/grammar-docs.test.ts +109 -0
- package/support/tests/parser-at-rules.test.ts +430 -0
- package/support/tests/parser-declarations.test.ts +152 -0
- package/support/tests/parser-edge-cases.test.ts +296 -0
- package/support/tests/parser-expressions.test.ts +413 -0
- package/support/tests/parser-real-world.test.ts +429 -0
- package/support/tests/parser-selectors.test.ts +169 -0
- package/support/tests/tokenizer.test.ts +268 -0
- package/tsconfig.json +5 -0
package/src/tables.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grammar tables, the data-driven parts of the Quark grammar, kept in one
|
|
3
|
+
* dependency-free module so the parser, the formatter, tooling, and the
|
|
4
|
+
* language reference (`support/docs/README.md`, locked by tests) read the
|
|
5
|
+
* same values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Binary operator binding powers (Pratt parser). Higher binds tighter; all
|
|
10
|
+
* operators are left-associative. `not` is unary and sits at `NOT_BP`.
|
|
11
|
+
*/
|
|
12
|
+
export const BINARY_BP: Readonly<Record<string, number>> = {
|
|
13
|
+
or: 1,
|
|
14
|
+
and: 2,
|
|
15
|
+
"==": 4,
|
|
16
|
+
"!=": 4,
|
|
17
|
+
"<": 5,
|
|
18
|
+
">": 5,
|
|
19
|
+
"<=": 5,
|
|
20
|
+
">=": 5,
|
|
21
|
+
"+": 6,
|
|
22
|
+
"-": 6,
|
|
23
|
+
"*": 7,
|
|
24
|
+
"/": 7,
|
|
25
|
+
"%": 7,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Binding power of the unary `not` operator (between `and` and `==`). */
|
|
29
|
+
export const NOT_BP = 3;
|
|
30
|
+
|
|
31
|
+
/** Attribute selector operators: `[name<op>value]`. */
|
|
32
|
+
export const ATTR_OPERATORS: ReadonlySet<string> = new Set([
|
|
33
|
+
"=",
|
|
34
|
+
"*=",
|
|
35
|
+
"^=",
|
|
36
|
+
"$=",
|
|
37
|
+
"|=",
|
|
38
|
+
"~=",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pseudo-classes whose argument parses as a selector list; every other
|
|
43
|
+
* pseudo-class argument is kept as raw text (`:nth-child(2n+1)`).
|
|
44
|
+
*/
|
|
45
|
+
export const SELECTOR_PSEUDOS: ReadonlySet<string> = new Set([
|
|
46
|
+
"not",
|
|
47
|
+
"is",
|
|
48
|
+
"where",
|
|
49
|
+
"has",
|
|
50
|
+
"matches",
|
|
51
|
+
"any",
|
|
52
|
+
"-webkit-any",
|
|
53
|
+
"-moz-any",
|
|
54
|
+
"host",
|
|
55
|
+
"host-context",
|
|
56
|
+
"current",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Quark's at-rules — the whole set. Each has a dedicated AST node and a
|
|
61
|
+
* parse method; any other name (`@media`, `@if`, `@keyframes`, …) is a
|
|
62
|
+
* parse error, since Quark is a derivative of CSS, not a superset.
|
|
63
|
+
*/
|
|
64
|
+
export const QUARK_AT_RULES = [
|
|
65
|
+
"use",
|
|
66
|
+
"scope",
|
|
67
|
+
"on",
|
|
68
|
+
"dispatch",
|
|
69
|
+
"command",
|
|
70
|
+
"view-transition",
|
|
71
|
+
"delay",
|
|
72
|
+
"warn",
|
|
73
|
+
"debug",
|
|
74
|
+
"error",
|
|
75
|
+
] as const;
|
|
76
|
+
|
|
77
|
+
export type QuarkAtRuleName = (typeof QUARK_AT_RULES)[number];
|
package/src/tokenizer.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { QuarkParseError } from "./error";
|
|
2
|
+
import type { Token, TokenizeResult } from "./types";
|
|
3
|
+
|
|
4
|
+
// Character codes used by the scanner.
|
|
5
|
+
const TAB = 9;
|
|
6
|
+
const LF = 10;
|
|
7
|
+
const FF = 12;
|
|
8
|
+
const CR = 13;
|
|
9
|
+
const SPACE = 32;
|
|
10
|
+
const BANG = 33; // !
|
|
11
|
+
const DQUOTE = 34; // "
|
|
12
|
+
const HASH = 35; // #
|
|
13
|
+
const DOLLAR = 36; // $
|
|
14
|
+
const PERCENT = 37; // %
|
|
15
|
+
const SQUOTE = 39; // '
|
|
16
|
+
const LPAREN = 40; // (
|
|
17
|
+
const RPAREN = 41; // )
|
|
18
|
+
const STAR = 42; // *
|
|
19
|
+
const PLUS = 43; // +
|
|
20
|
+
const MINUS = 45; // -
|
|
21
|
+
const DOT = 46; // .
|
|
22
|
+
const SLASH = 47; // /
|
|
23
|
+
const COLON = 58; // :
|
|
24
|
+
const LT = 60; // <
|
|
25
|
+
const EQ = 61; // =
|
|
26
|
+
const GT = 62; // >
|
|
27
|
+
const AT = 64; // @
|
|
28
|
+
const BACKSLASH = 92; // \
|
|
29
|
+
const CARET = 94; // ^
|
|
30
|
+
const UNDERSCORE = 95; // _
|
|
31
|
+
const LBRACE = 123; // {
|
|
32
|
+
const PIPE = 124; // |
|
|
33
|
+
const RBRACE = 125; // }
|
|
34
|
+
const TILDE = 126; // ~
|
|
35
|
+
|
|
36
|
+
const isWs = (c: number): boolean =>
|
|
37
|
+
c === SPACE || c === TAB || c === LF || c === CR || c === FF;
|
|
38
|
+
const isDigit = (c: number): boolean => c >= 48 && c <= 57;
|
|
39
|
+
const isIdentStart = (c: number): boolean =>
|
|
40
|
+
(c >= 97 && c <= 122) ||
|
|
41
|
+
(c >= 65 && c <= 90) ||
|
|
42
|
+
c === UNDERSCORE ||
|
|
43
|
+
c === BACKSLASH ||
|
|
44
|
+
c >= 0x80;
|
|
45
|
+
const isIdentChar = (c: number): boolean =>
|
|
46
|
+
isIdentStart(c) || isDigit(c) || c === MINUS;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Single-pass tokenizer. Whitespace is not tokenized; each token carries a
|
|
50
|
+
* `ws` flag indicating whether whitespace/comments preceded it. Comments are
|
|
51
|
+
* returned separately so expression parsing never has to skip them.
|
|
52
|
+
*/
|
|
53
|
+
export function tokenize(source: string): TokenizeResult {
|
|
54
|
+
const tokens: Token[] = [];
|
|
55
|
+
const comments: Token[] = [];
|
|
56
|
+
const len = source.length;
|
|
57
|
+
let i = 0;
|
|
58
|
+
let ws = false;
|
|
59
|
+
|
|
60
|
+
const err = (message: string, at: number): never => {
|
|
61
|
+
throw new QuarkParseError(message, source, at);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const push = (
|
|
65
|
+
type: Token["type"],
|
|
66
|
+
value: string,
|
|
67
|
+
start: number,
|
|
68
|
+
end: number,
|
|
69
|
+
unit?: string,
|
|
70
|
+
quote?: '"' | "'"
|
|
71
|
+
): void => {
|
|
72
|
+
const token: Token = { type, value, start, end, ws };
|
|
73
|
+
if (unit !== undefined) token.unit = unit;
|
|
74
|
+
if (quote !== undefined) token.quote = quote;
|
|
75
|
+
tokens.push(token);
|
|
76
|
+
ws = false;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** True when the previous token can end a value (for `-`/`+` handling). */
|
|
80
|
+
const prevValueLike = (): boolean => {
|
|
81
|
+
const t = tokens[tokens.length - 1];
|
|
82
|
+
if (!t) return false;
|
|
83
|
+
if (t.type === "punct") return t.value === ")" || t.value === "]";
|
|
84
|
+
return t.type !== "at";
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Skips a balanced `#{...}`; `idx` points just after the `#{`. */
|
|
88
|
+
const skipInterpolation = (idx: number): number => {
|
|
89
|
+
let depth = 1;
|
|
90
|
+
while (idx < len && depth > 0) {
|
|
91
|
+
const c = source.charCodeAt(idx);
|
|
92
|
+
if (c === LBRACE) depth++;
|
|
93
|
+
else if (c === RBRACE) depth--;
|
|
94
|
+
else if (c === DQUOTE || c === SQUOTE) {
|
|
95
|
+
idx++;
|
|
96
|
+
while (idx < len && source.charCodeAt(idx) !== c) {
|
|
97
|
+
if (source.charCodeAt(idx) === BACKSLASH) idx++;
|
|
98
|
+
idx++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
idx++;
|
|
102
|
+
}
|
|
103
|
+
return idx;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const scanIdent = (start: number): string => {
|
|
107
|
+
let j = start;
|
|
108
|
+
while (j < len) {
|
|
109
|
+
const c = source.charCodeAt(j);
|
|
110
|
+
if (c === BACKSLASH) {
|
|
111
|
+
j += 2;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!isIdentChar(c)) break;
|
|
115
|
+
j++;
|
|
116
|
+
}
|
|
117
|
+
i = j;
|
|
118
|
+
return source.slice(start, j);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const scanString = (quote: number): void => {
|
|
122
|
+
const start = i;
|
|
123
|
+
i++;
|
|
124
|
+
while (i < len) {
|
|
125
|
+
const c = source.charCodeAt(i);
|
|
126
|
+
if (c === quote) break;
|
|
127
|
+
if (c === BACKSLASH) {
|
|
128
|
+
i += 2;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (c === HASH && source.charCodeAt(i + 1) === LBRACE) {
|
|
132
|
+
i = skipInterpolation(i + 2);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
if (i >= len) err("Unterminated string", start);
|
|
138
|
+
push(
|
|
139
|
+
"string",
|
|
140
|
+
source.slice(start + 1, i),
|
|
141
|
+
start,
|
|
142
|
+
i + 1,
|
|
143
|
+
undefined,
|
|
144
|
+
quote === DQUOTE ? '"' : "'"
|
|
145
|
+
);
|
|
146
|
+
i++;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** `start` may point at a sign, a leading dot, or a digit. */
|
|
150
|
+
const scanNumber = (start: number): void => {
|
|
151
|
+
let j = start;
|
|
152
|
+
const first = source.charCodeAt(j);
|
|
153
|
+
if (first === MINUS || first === PLUS) j++;
|
|
154
|
+
while (isDigit(source.charCodeAt(j))) j++;
|
|
155
|
+
if (source.charCodeAt(j) === DOT && isDigit(source.charCodeAt(j + 1))) {
|
|
156
|
+
j++;
|
|
157
|
+
while (isDigit(source.charCodeAt(j))) j++;
|
|
158
|
+
}
|
|
159
|
+
// Exponent (`2e3`, `2e-3`), but not units that start with `e` (`2em`).
|
|
160
|
+
const e = source.charCodeAt(j);
|
|
161
|
+
if (e === 101 || e === 69) {
|
|
162
|
+
let k = j + 1;
|
|
163
|
+
const s = source.charCodeAt(k);
|
|
164
|
+
if (s === MINUS || s === PLUS) k++;
|
|
165
|
+
if (isDigit(source.charCodeAt(k))) {
|
|
166
|
+
k++;
|
|
167
|
+
while (isDigit(source.charCodeAt(k))) k++;
|
|
168
|
+
j = k;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const numEnd = j;
|
|
172
|
+
let unit: string | undefined;
|
|
173
|
+
if (source.charCodeAt(j) === PERCENT) {
|
|
174
|
+
unit = "%";
|
|
175
|
+
j++;
|
|
176
|
+
} else if (isIdentStart(source.charCodeAt(j))) {
|
|
177
|
+
const u = j;
|
|
178
|
+
while (j < len && isIdentChar(source.charCodeAt(j))) j++;
|
|
179
|
+
unit = source.slice(u, j);
|
|
180
|
+
}
|
|
181
|
+
push("number", source.slice(start, numEnd), start, j, unit);
|
|
182
|
+
i = j;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Attempts to scan the contents of an unquoted `url(...)`. Returns `null`
|
|
187
|
+
* (without emitting) when the contents look like a normal expression
|
|
188
|
+
* (quoted string, variable, ...), in which case regular tokenization
|
|
189
|
+
* continues from the `(`.
|
|
190
|
+
*/
|
|
191
|
+
const tryScanRawUrl = (): {
|
|
192
|
+
rawStart: number;
|
|
193
|
+
rawEnd: number;
|
|
194
|
+
endAfterParen: number;
|
|
195
|
+
} | null => {
|
|
196
|
+
let j = i + 1; // after "("
|
|
197
|
+
while (j < len && isWs(source.charCodeAt(j))) j++;
|
|
198
|
+
const q = source.charCodeAt(j);
|
|
199
|
+
if (q === DQUOTE || q === SQUOTE || q === DOLLAR || q === RPAREN) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const rawStart = j;
|
|
203
|
+
let lastNonWs = j - 1;
|
|
204
|
+
let sawWs = false;
|
|
205
|
+
while (j < len) {
|
|
206
|
+
const c = source.charCodeAt(j);
|
|
207
|
+
if (c === RPAREN) break;
|
|
208
|
+
if (c === BACKSLASH) {
|
|
209
|
+
j += 2;
|
|
210
|
+
lastNonWs = j - 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (c === HASH && source.charCodeAt(j + 1) === LBRACE) {
|
|
214
|
+
j = skipInterpolation(j + 2);
|
|
215
|
+
lastNonWs = j - 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (c === DQUOTE || c === SQUOTE || c === LPAREN || c === DOLLAR) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (isWs(c)) {
|
|
222
|
+
sawWs = true;
|
|
223
|
+
} else {
|
|
224
|
+
if (sawWs) return null; // internal whitespace: not a raw url
|
|
225
|
+
lastNonWs = j;
|
|
226
|
+
}
|
|
227
|
+
j++;
|
|
228
|
+
}
|
|
229
|
+
if (j >= len || lastNonWs < rawStart) return null;
|
|
230
|
+
return { rawStart, rawEnd: lastNonWs + 1, endAfterParen: j + 1 };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
while (i < len) {
|
|
234
|
+
const c = source.charCodeAt(i);
|
|
235
|
+
|
|
236
|
+
if (isWs(c)) {
|
|
237
|
+
i++;
|
|
238
|
+
ws = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// `/* */` comments and the `/` operator.
|
|
243
|
+
if (c === SLASH) {
|
|
244
|
+
const n = source.charCodeAt(i + 1);
|
|
245
|
+
if (n === SLASH) err("Line comments are not supported, use /* */", i);
|
|
246
|
+
if (n === STAR) {
|
|
247
|
+
const start = i;
|
|
248
|
+
i += 2;
|
|
249
|
+
while (
|
|
250
|
+
i < len &&
|
|
251
|
+
!(source.charCodeAt(i) === STAR && source.charCodeAt(i + 1) === SLASH)
|
|
252
|
+
) {
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
if (i >= len) err("Unterminated comment", start);
|
|
256
|
+
comments.push({
|
|
257
|
+
type: "comment",
|
|
258
|
+
value: source.slice(start + 2, i),
|
|
259
|
+
start,
|
|
260
|
+
end: i + 2,
|
|
261
|
+
ws,
|
|
262
|
+
});
|
|
263
|
+
i += 2;
|
|
264
|
+
ws = true;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
push("punct", "/", i, i + 1);
|
|
268
|
+
i++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (c === DQUOTE || c === SQUOTE) {
|
|
273
|
+
scanString(c);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (isDigit(c)) {
|
|
278
|
+
scanNumber(i);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (c === DOT) {
|
|
283
|
+
if (isDigit(source.charCodeAt(i + 1))) {
|
|
284
|
+
scanNumber(i);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
source.charCodeAt(i + 1) === DOT &&
|
|
289
|
+
source.charCodeAt(i + 2) === DOT
|
|
290
|
+
) {
|
|
291
|
+
push("punct", "...", i, i + 3);
|
|
292
|
+
i += 3;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
push("punct", ".", i, i + 1);
|
|
296
|
+
i++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (c === MINUS || c === PLUS) {
|
|
301
|
+
const n = source.charCodeAt(i + 1);
|
|
302
|
+
const startsNumber =
|
|
303
|
+
isDigit(n) || (n === DOT && isDigit(source.charCodeAt(i + 2)));
|
|
304
|
+
/*
|
|
305
|
+
* A sign when at an expression start, or when in the `10px -5px`
|
|
306
|
+
* shape (whitespace before, none after). `10-5` and `10 - 5` stay
|
|
307
|
+
* subtraction.
|
|
308
|
+
*/
|
|
309
|
+
const isSign = !prevValueLike() || (ws && !isWs(n));
|
|
310
|
+
if (startsNumber && isSign) {
|
|
311
|
+
scanNumber(i);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (c === MINUS && (isIdentStart(n) || n === MINUS) && isSign) {
|
|
315
|
+
const start = i;
|
|
316
|
+
i++; // include the '-' via slice below
|
|
317
|
+
scanIdent(i);
|
|
318
|
+
push("ident", source.slice(start, i), start, i);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
push("punct", c === MINUS ? "-" : "+", i, i + 1);
|
|
322
|
+
i++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (c === DOLLAR) {
|
|
327
|
+
const n = source.charCodeAt(i + 1);
|
|
328
|
+
if (isIdentStart(n) || isDigit(n)) {
|
|
329
|
+
const start = i;
|
|
330
|
+
const value = scanIdent(i + 1);
|
|
331
|
+
push("variable", value, start, i);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (n === EQ) {
|
|
335
|
+
push("punct", "$=", i, i + 2);
|
|
336
|
+
i += 2;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
push("punct", "$", i, i + 1);
|
|
340
|
+
i++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (c === AT) {
|
|
345
|
+
const n = source.charCodeAt(i + 1);
|
|
346
|
+
if (isIdentStart(n) || n === MINUS) {
|
|
347
|
+
const start = i;
|
|
348
|
+
const value = scanIdent(i + 1);
|
|
349
|
+
push("at", value, start, i);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
push("punct", "@", i, i + 1);
|
|
353
|
+
i++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (c === HASH) {
|
|
358
|
+
const n = source.charCodeAt(i + 1);
|
|
359
|
+
if (n === LBRACE) {
|
|
360
|
+
push("punct", "#{", i, i + 2);
|
|
361
|
+
i += 2;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (isIdentChar(n)) {
|
|
365
|
+
const start = i;
|
|
366
|
+
let j = i + 1;
|
|
367
|
+
while (j < len && isIdentChar(source.charCodeAt(j))) j++;
|
|
368
|
+
push("hash", source.slice(start + 1, j), start, j);
|
|
369
|
+
i = j;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
push("punct", "#", i, i + 1);
|
|
373
|
+
i++;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (isIdentStart(c)) {
|
|
378
|
+
const start = i;
|
|
379
|
+
const value = scanIdent(start);
|
|
380
|
+
if (
|
|
381
|
+
source.charCodeAt(i) === LPAREN &&
|
|
382
|
+
(value === "url" || value === "URL" || value === "Url")
|
|
383
|
+
) {
|
|
384
|
+
const parenAt = i;
|
|
385
|
+
const url = tryScanRawUrl();
|
|
386
|
+
if (url) {
|
|
387
|
+
push("ident", value, start, parenAt);
|
|
388
|
+
push("punct", "(", parenAt, parenAt + 1);
|
|
389
|
+
push(
|
|
390
|
+
"url",
|
|
391
|
+
source.slice(url.rawStart, url.rawEnd),
|
|
392
|
+
url.rawStart,
|
|
393
|
+
url.rawEnd
|
|
394
|
+
);
|
|
395
|
+
push("punct", ")", url.endAfterParen - 1, url.endAfterParen);
|
|
396
|
+
i = url.endAfterParen;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
push("ident", value, start, i);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Multi-character and single-character punctuation.
|
|
405
|
+
const n = source.charCodeAt(i + 1);
|
|
406
|
+
let punct: string | null = null;
|
|
407
|
+
switch (c) {
|
|
408
|
+
case EQ:
|
|
409
|
+
punct = n === EQ ? "==" : "=";
|
|
410
|
+
break;
|
|
411
|
+
case BANG:
|
|
412
|
+
punct = n === EQ ? "!=" : "!";
|
|
413
|
+
break;
|
|
414
|
+
case LT:
|
|
415
|
+
punct = n === EQ ? "<=" : "<";
|
|
416
|
+
break;
|
|
417
|
+
case GT:
|
|
418
|
+
punct = n === EQ ? ">=" : ">";
|
|
419
|
+
break;
|
|
420
|
+
case COLON:
|
|
421
|
+
punct = n === COLON ? "::" : ":";
|
|
422
|
+
break;
|
|
423
|
+
case STAR:
|
|
424
|
+
punct = n === EQ ? "*=" : "*";
|
|
425
|
+
break;
|
|
426
|
+
case TILDE:
|
|
427
|
+
punct = n === EQ ? "~=" : "~";
|
|
428
|
+
break;
|
|
429
|
+
case CARET:
|
|
430
|
+
punct = n === EQ ? "^=" : "^";
|
|
431
|
+
break;
|
|
432
|
+
case PIPE:
|
|
433
|
+
punct = n === EQ ? "|=" : "|";
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
punct = source[i];
|
|
437
|
+
}
|
|
438
|
+
push("punct", punct, i, i + punct.length);
|
|
439
|
+
i += punct.length;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { tokens, comments };
|
|
443
|
+
}
|