UNPKG

16.9 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8/**
9 * @typedef {Object} CssTokenCallbacks
10 * @property {function(string, number): boolean} isSelector
11 * @property {function(string, number, number, number, number): number=} url
12 * @property {function(string, number, number): number=} string
13 * @property {function(string, number, number): number=} leftParenthesis
14 * @property {function(string, number, number): number=} rightParenthesis
15 * @property {function(string, number, number): number=} pseudoFunction
16 * @property {function(string, number, number): number=} function
17 * @property {function(string, number, number): number=} pseudoClass
18 * @property {function(string, number, number): number=} atKeyword
19 * @property {function(string, number, number): number=} class
20 * @property {function(string, number, number): number=} identifier
21 * @property {function(string, number, number): number=} id
22 * @property {function(string, number, number): number=} leftCurlyBracket
23 * @property {function(string, number, number): number=} rightCurlyBracket
24 * @property {function(string, number, number): number=} semicolon
25 * @property {function(string, number, number): number=} comma
26 */
27
28/** @typedef {function(string, number, CssTokenCallbacks): number} CharHandler */
29
30// spec: https://drafts.csswg.org/css-syntax/
31
32const CC_LINE_FEED = "\n".charCodeAt(0);
33const CC_CARRIAGE_RETURN = "\r".charCodeAt(0);
34const CC_FORM_FEED = "\f".charCodeAt(0);
35
36const CC_TAB = "\t".charCodeAt(0);
37const CC_SPACE = " ".charCodeAt(0);
38
39const CC_SLASH = "/".charCodeAt(0);
40const CC_BACK_SLASH = "\\".charCodeAt(0);
41const CC_ASTERISK = "*".charCodeAt(0);
42
43const CC_LEFT_PARENTHESIS = "(".charCodeAt(0);
44const CC_RIGHT_PARENTHESIS = ")".charCodeAt(0);
45const CC_LEFT_CURLY = "{".charCodeAt(0);
46const CC_RIGHT_CURLY = "}".charCodeAt(0);
47
48const CC_QUOTATION_MARK = '"'.charCodeAt(0);
49const CC_APOSTROPHE = "'".charCodeAt(0);
50
51const CC_FULL_STOP = ".".charCodeAt(0);
52const CC_COLON = ":".charCodeAt(0);
53const CC_SEMICOLON = ";".charCodeAt(0);
54const CC_COMMA = ",".charCodeAt(0);
55const CC_PERCENTAGE = "%".charCodeAt(0);
56const CC_AT_SIGN = "@".charCodeAt(0);
57
58const CC_LOW_LINE = "_".charCodeAt(0);
59const CC_LOWER_A = "a".charCodeAt(0);
60const CC_LOWER_U = "u".charCodeAt(0);
61const CC_LOWER_E = "e".charCodeAt(0);
62const CC_LOWER_Z = "z".charCodeAt(0);
63const CC_UPPER_A = "A".charCodeAt(0);
64const CC_UPPER_E = "E".charCodeAt(0);
65const CC_UPPER_Z = "Z".charCodeAt(0);
66const CC_0 = "0".charCodeAt(0);
67const CC_9 = "9".charCodeAt(0);
68
69const CC_NUMBER_SIGN = "#".charCodeAt(0);
70const CC_PLUS_SIGN = "+".charCodeAt(0);
71const CC_HYPHEN_MINUS = "-".charCodeAt(0);
72
73const CC_LESS_THAN_SIGN = "<".charCodeAt(0);
74const CC_GREATER_THAN_SIGN = ">".charCodeAt(0);
75
76const _isNewLine = cc => {
77 return (
78 cc === CC_LINE_FEED || cc === CC_CARRIAGE_RETURN || cc === CC_FORM_FEED
79 );
80};
81
82/** @type {CharHandler} */
83const consumeSpace = (input, pos, callbacks) => {
84 let cc;
85 do {
86 pos++;
87 cc = input.charCodeAt(pos);
88 } while (_isWhiteSpace(cc));
89 return pos;
90};
91
92const _isWhiteSpace = cc => {
93 return (
94 cc === CC_LINE_FEED ||
95 cc === CC_CARRIAGE_RETURN ||
96 cc === CC_FORM_FEED ||
97 cc === CC_TAB ||
98 cc === CC_SPACE
99 );
100};
101
102/** @type {CharHandler} */
103const consumeSingleCharToken = (input, pos, callbacks) => {
104 return pos + 1;
105};
106
107/** @type {CharHandler} */
108const consumePotentialComment = (input, pos, callbacks) => {
109 pos++;
110 if (pos === input.length) return pos;
111 let cc = input.charCodeAt(pos);
112 if (cc !== CC_ASTERISK) return pos;
113 for (;;) {
114 pos++;
115 if (pos === input.length) return pos;
116 cc = input.charCodeAt(pos);
117 while (cc === CC_ASTERISK) {
118 pos++;
119 if (pos === input.length) return pos;
120 cc = input.charCodeAt(pos);
121 if (cc === CC_SLASH) return pos + 1;
122 }
123 }
124};
125
126/** @type {function(number): CharHandler} */
127const consumeString = end => (input, pos, callbacks) => {
128 const start = pos;
129 pos = _consumeString(input, pos, end);
130 if (callbacks.string !== undefined) {
131 pos = callbacks.string(input, start, pos);
132 }
133 return pos;
134};
135
136const _consumeString = (input, pos, end) => {
137 pos++;
138 for (;;) {
139 if (pos === input.length) return pos;
140 const cc = input.charCodeAt(pos);
141 if (cc === end) return pos + 1;
142 if (_isNewLine(cc)) {
143 // bad string
144 return pos;
145 }
146 if (cc === CC_BACK_SLASH) {
147 // we don't need to fully parse the escaped code point
148 // just skip over a potential new line
149 pos++;
150 if (pos === input.length) return pos;
151 pos++;
152 } else {
153 pos++;
154 }
155 }
156};
157
158const _isIdentifierStartCode = cc => {
159 return (
160 cc === CC_LOW_LINE ||
161 (cc >= CC_LOWER_A && cc <= CC_LOWER_Z) ||
162 (cc >= CC_UPPER_A && cc <= CC_UPPER_Z) ||
163 cc > 0x80
164 );
165};
166
167const _isDigit = cc => {
168 return cc >= CC_0 && cc <= CC_9;
169};
170
171const _startsIdentifier = (input, pos) => {
172 const cc = input.charCodeAt(pos);
173 if (cc === CC_HYPHEN_MINUS) {
174 if (pos === input.length) return false;
175 const cc = input.charCodeAt(pos + 1);
176 if (cc === CC_HYPHEN_MINUS) return true;
177 if (cc === CC_BACK_SLASH) {
178 const cc = input.charCodeAt(pos + 2);
179 return !_isNewLine(cc);
180 }
181 return _isIdentifierStartCode(cc);
182 }
183 if (cc === CC_BACK_SLASH) {
184 const cc = input.charCodeAt(pos + 1);
185 return !_isNewLine(cc);
186 }
187 return _isIdentifierStartCode(cc);
188};
189
190/** @type {CharHandler} */
191const consumeNumberSign = (input, pos, callbacks) => {
192 const start = pos;
193 pos++;
194 if (pos === input.length) return pos;
195 if (callbacks.isSelector(input, pos) && _startsIdentifier(input, pos)) {
196 pos = _consumeIdentifier(input, pos);
197 if (callbacks.id !== undefined) {
198 return callbacks.id(input, start, pos);
199 }
200 }
201 return pos;
202};
203
204/** @type {CharHandler} */
205const consumeMinus = (input, pos, callbacks) => {
206 const start = pos;
207 pos++;
208 if (pos === input.length) return pos;
209 const cc = input.charCodeAt(pos);
210 if (cc === CC_FULL_STOP || _isDigit(cc)) {
211 return consumeNumericToken(input, pos, callbacks);
212 } else if (cc === CC_HYPHEN_MINUS) {
213 pos++;
214 if (pos === input.length) return pos;
215 const cc = input.charCodeAt(pos);
216 if (cc === CC_GREATER_THAN_SIGN) {
217 return pos + 1;
218 } else {
219 pos = _consumeIdentifier(input, pos);
220 if (callbacks.identifier !== undefined) {
221 return callbacks.identifier(input, start, pos);
222 }
223 }
224 } else if (cc === CC_BACK_SLASH) {
225 if (pos + 1 === input.length) return pos;
226 const cc = input.charCodeAt(pos + 1);
227 if (_isNewLine(cc)) return pos;
228 pos = _consumeIdentifier(input, pos);
229 if (callbacks.identifier !== undefined) {
230 return callbacks.identifier(input, start, pos);
231 }
232 } else if (_isIdentifierStartCode(cc)) {
233 pos++;
234 pos = _consumeIdentifier(input, pos);
235 if (callbacks.identifier !== undefined) {
236 return callbacks.identifier(input, start, pos);
237 }
238 }
239 return pos;
240};
241
242/** @type {CharHandler} */
243const consumeDot = (input, pos, callbacks) => {
244 const start = pos;
245 pos++;
246 if (pos === input.length) return pos;
247 const cc = input.charCodeAt(pos);
248 if (_isDigit(cc)) return consumeNumericToken(input, pos - 2, callbacks);
249 if (!callbacks.isSelector(input, pos) || !_startsIdentifier(input, pos))
250 return pos;
251 pos = _consumeIdentifier(input, pos);
252 if (callbacks.class !== undefined) return callbacks.class(input, start, pos);
253 return pos;
254};
255
256/** @type {CharHandler} */
257const consumeNumericToken = (input, pos, callbacks) => {
258 pos = _consumeNumber(input, pos);
259 if (pos === input.length) return pos;
260 if (_startsIdentifier(input, pos)) return _consumeIdentifier(input, pos);
261 const cc = input.charCodeAt(pos);
262 if (cc === CC_PERCENTAGE) return pos + 1;
263 return pos;
264};
265
266/** @type {CharHandler} */
267const consumeOtherIdentifier = (input, pos, callbacks) => {
268 const start = pos;
269 pos = _consumeIdentifier(input, pos);
270 if (
271 pos !== input.length &&
272 !callbacks.isSelector(input, pos) &&
273 input.charCodeAt(pos) === CC_LEFT_PARENTHESIS
274 ) {
275 pos++;
276 if (callbacks.function !== undefined) {
277 return callbacks.function(input, start, pos);
278 }
279 } else {
280 if (callbacks.identifier !== undefined) {
281 return callbacks.identifier(input, start, pos);
282 }
283 }
284 return pos;
285};
286
287/** @type {CharHandler} */
288const consumePotentialUrl = (input, pos, callbacks) => {
289 const start = pos;
290 pos = _consumeIdentifier(input, pos);
291 if (pos === start + 3 && input.slice(start, pos + 1) === "url(") {
292 pos++;
293 let cc = input.charCodeAt(pos);
294 while (_isWhiteSpace(cc)) {
295 pos++;
296 if (pos === input.length) return pos;
297 cc = input.charCodeAt(pos);
298 }
299 if (cc === CC_QUOTATION_MARK || cc === CC_APOSTROPHE) {
300 pos++;
301 const contentStart = pos;
302 pos = _consumeString(input, pos, cc);
303 const contentEnd = pos - 1;
304 cc = input.charCodeAt(pos);
305 while (_isWhiteSpace(cc)) {
306 pos++;
307 if (pos === input.length) return pos;
308 cc = input.charCodeAt(pos);
309 }
310 if (cc !== CC_RIGHT_PARENTHESIS) return pos;
311 pos++;
312 if (callbacks.url !== undefined)
313 return callbacks.url(input, start, pos, contentStart, contentEnd);
314 return pos;
315 } else {
316 const contentStart = pos;
317 let contentEnd;
318 for (;;) {
319 if (cc === CC_BACK_SLASH) {
320 pos++;
321 if (pos === input.length) return pos;
322 pos++;
323 } else if (_isWhiteSpace(cc)) {
324 contentEnd = pos;
325 do {
326 pos++;
327 if (pos === input.length) return pos;
328 cc = input.charCodeAt(pos);
329 } while (_isWhiteSpace(cc));
330 if (cc !== CC_RIGHT_PARENTHESIS) return pos;
331 pos++;
332 if (callbacks.url !== undefined) {
333 return callbacks.url(input, start, pos, contentStart, contentEnd);
334 }
335 return pos;
336 } else if (cc === CC_RIGHT_PARENTHESIS) {
337 contentEnd = pos;
338 pos++;
339 if (callbacks.url !== undefined) {
340 return callbacks.url(input, start, pos, contentStart, contentEnd);
341 }
342 return pos;
343 } else if (cc === CC_LEFT_PARENTHESIS) {
344 return pos;
345 } else {
346 pos++;
347 }
348 if (pos === input.length) return pos;
349 cc = input.charCodeAt(pos);
350 }
351 }
352 } else {
353 if (callbacks.identifier !== undefined) {
354 return callbacks.identifier(input, start, pos);
355 }
356 return pos;
357 }
358};
359
360/** @type {CharHandler} */
361const consumePotentialPseudo = (input, pos, callbacks) => {
362 const start = pos;
363 pos++;
364 if (!callbacks.isSelector(input, pos) || !_startsIdentifier(input, pos))
365 return pos;
366 pos = _consumeIdentifier(input, pos);
367 let cc = input.charCodeAt(pos);
368 if (cc === CC_LEFT_PARENTHESIS) {
369 pos++;
370 if (callbacks.pseudoFunction !== undefined) {
371 return callbacks.pseudoFunction(input, start, pos);
372 }
373 return pos;
374 }
375 if (callbacks.pseudoClass !== undefined) {
376 return callbacks.pseudoClass(input, start, pos);
377 }
378 return pos;
379};
380
381/** @type {CharHandler} */
382const consumeLeftParenthesis = (input, pos, callbacks) => {
383 pos++;
384 if (callbacks.leftParenthesis !== undefined) {
385 return callbacks.leftParenthesis(input, pos - 1, pos);
386 }
387 return pos;
388};
389
390/** @type {CharHandler} */
391const consumeRightParenthesis = (input, pos, callbacks) => {
392 pos++;
393 if (callbacks.rightParenthesis !== undefined) {
394 return callbacks.rightParenthesis(input, pos - 1, pos);
395 }
396 return pos;
397};
398
399/** @type {CharHandler} */
400const consumeLeftCurlyBracket = (input, pos, callbacks) => {
401 pos++;
402 if (callbacks.leftCurlyBracket !== undefined) {
403 return callbacks.leftCurlyBracket(input, pos - 1, pos);
404 }
405 return pos;
406};
407
408/** @type {CharHandler} */
409const consumeRightCurlyBracket = (input, pos, callbacks) => {
410 pos++;
411 if (callbacks.rightCurlyBracket !== undefined) {
412 return callbacks.rightCurlyBracket(input, pos - 1, pos);
413 }
414 return pos;
415};
416
417/** @type {CharHandler} */
418const consumeSemicolon = (input, pos, callbacks) => {
419 pos++;
420 if (callbacks.semicolon !== undefined) {
421 return callbacks.semicolon(input, pos - 1, pos);
422 }
423 return pos;
424};
425
426/** @type {CharHandler} */
427const consumeComma = (input, pos, callbacks) => {
428 pos++;
429 if (callbacks.comma !== undefined) {
430 return callbacks.comma(input, pos - 1, pos);
431 }
432 return pos;
433};
434
435const _consumeIdentifier = (input, pos) => {
436 for (;;) {
437 const cc = input.charCodeAt(pos);
438 if (cc === CC_BACK_SLASH) {
439 pos++;
440 if (pos === input.length) return pos;
441 pos++;
442 } else if (
443 _isIdentifierStartCode(cc) ||
444 _isDigit(cc) ||
445 cc === CC_HYPHEN_MINUS
446 ) {
447 pos++;
448 } else {
449 return pos;
450 }
451 }
452};
453
454const _consumeNumber = (input, pos) => {
455 pos++;
456 if (pos === input.length) return pos;
457 let cc = input.charCodeAt(pos);
458 while (_isDigit(cc)) {
459 pos++;
460 if (pos === input.length) return pos;
461 cc = input.charCodeAt(pos);
462 }
463 if (cc === CC_FULL_STOP && pos + 1 !== input.length) {
464 const next = input.charCodeAt(pos + 1);
465 if (_isDigit(next)) {
466 pos += 2;
467 cc = input.charCodeAt(pos);
468 while (_isDigit(cc)) {
469 pos++;
470 if (pos === input.length) return pos;
471 cc = input.charCodeAt(pos);
472 }
473 }
474 }
475 if (cc === CC_LOWER_E || cc === CC_UPPER_E) {
476 if (pos + 1 !== input.length) {
477 const next = input.charCodeAt(pos + 2);
478 if (_isDigit(next)) {
479 pos += 2;
480 } else if (
481 (next === CC_HYPHEN_MINUS || next === CC_PLUS_SIGN) &&
482 pos + 2 !== input.length
483 ) {
484 const next = input.charCodeAt(pos + 2);
485 if (_isDigit(next)) {
486 pos += 3;
487 } else {
488 return pos;
489 }
490 } else {
491 return pos;
492 }
493 }
494 } else {
495 return pos;
496 }
497 cc = input.charCodeAt(pos);
498 while (_isDigit(cc)) {
499 pos++;
500 if (pos === input.length) return pos;
501 cc = input.charCodeAt(pos);
502 }
503 return pos;
504};
505
506/** @type {CharHandler} */
507const consumeLessThan = (input, pos, callbacks) => {
508 if (input.slice(pos + 1, pos + 4) === "!--") return pos + 4;
509 return pos + 1;
510};
511
512/** @type {CharHandler} */
513const consumeAt = (input, pos, callbacks) => {
514 const start = pos;
515 pos++;
516 if (pos === input.length) return pos;
517 if (_startsIdentifier(input, pos)) {
518 pos = _consumeIdentifier(input, pos);
519 if (callbacks.atKeyword !== undefined) {
520 pos = callbacks.atKeyword(input, start, pos);
521 }
522 }
523 return pos;
524};
525
526const CHAR_MAP = Array.from({ length: 0x80 }, (_, cc) => {
527 // https://drafts.csswg.org/css-syntax/#consume-token
528 switch (cc) {
529 case CC_LINE_FEED:
530 case CC_CARRIAGE_RETURN:
531 case CC_FORM_FEED:
532 case CC_TAB:
533 case CC_SPACE:
534 return consumeSpace;
535 case CC_QUOTATION_MARK:
536 case CC_APOSTROPHE:
537 return consumeString(cc);
538 case CC_NUMBER_SIGN:
539 return consumeNumberSign;
540 case CC_SLASH:
541 return consumePotentialComment;
542 // case CC_LEFT_SQUARE:
543 // case CC_RIGHT_SQUARE:
544 // case CC_COMMA:
545 // case CC_COLON:
546 // return consumeSingleCharToken;
547 case CC_COMMA:
548 return consumeComma;
549 case CC_SEMICOLON:
550 return consumeSemicolon;
551 case CC_LEFT_PARENTHESIS:
552 return consumeLeftParenthesis;
553 case CC_RIGHT_PARENTHESIS:
554 return consumeRightParenthesis;
555 case CC_LEFT_CURLY:
556 return consumeLeftCurlyBracket;
557 case CC_RIGHT_CURLY:
558 return consumeRightCurlyBracket;
559 case CC_COLON:
560 return consumePotentialPseudo;
561 case CC_PLUS_SIGN:
562 return consumeNumericToken;
563 case CC_FULL_STOP:
564 return consumeDot;
565 case CC_HYPHEN_MINUS:
566 return consumeMinus;
567 case CC_LESS_THAN_SIGN:
568 return consumeLessThan;
569 case CC_AT_SIGN:
570 return consumeAt;
571 case CC_LOWER_U:
572 return consumePotentialUrl;
573 case CC_LOW_LINE:
574 return consumeOtherIdentifier;
575 default:
576 if (_isDigit(cc)) return consumeNumericToken;
577 if (
578 (cc >= CC_LOWER_A && cc <= CC_LOWER_Z) ||
579 (cc >= CC_UPPER_A && cc <= CC_UPPER_Z)
580 ) {
581 return consumeOtherIdentifier;
582 }
583 return consumeSingleCharToken;
584 }
585});
586
587/**
588 * @param {string} input input css
589 * @param {CssTokenCallbacks} callbacks callbacks
590 * @returns {void}
591 */
592module.exports = (input, callbacks) => {
593 let pos = 0;
594 while (pos < input.length) {
595 const cc = input.charCodeAt(pos);
596 if (cc < 0x80) {
597 pos = CHAR_MAP[cc](input, pos, callbacks);
598 } else {
599 pos++;
600 }
601 }
602};
603
604module.exports.eatComments = (input, pos) => {
605 loop: for (;;) {
606 const cc = input.charCodeAt(pos);
607 if (cc === CC_SLASH) {
608 if (pos === input.length) return pos;
609 let cc = input.charCodeAt(pos + 1);
610 if (cc !== CC_ASTERISK) return pos;
611 pos++;
612 for (;;) {
613 pos++;
614 if (pos === input.length) return pos;
615 cc = input.charCodeAt(pos);
616 while (cc === CC_ASTERISK) {
617 pos++;
618 if (pos === input.length) return pos;
619 cc = input.charCodeAt(pos);
620 if (cc === CC_SLASH) {
621 pos++;
622 continue loop;
623 }
624 }
625 }
626 }
627 return pos;
628 }
629};
630
631module.exports.eatWhitespaceAndComments = (input, pos) => {
632 loop: for (;;) {
633 const cc = input.charCodeAt(pos);
634 if (cc === CC_SLASH) {
635 if (pos === input.length) return pos;
636 let cc = input.charCodeAt(pos + 1);
637 if (cc !== CC_ASTERISK) return pos;
638 pos++;
639 for (;;) {
640 pos++;
641 if (pos === input.length) return pos;
642 cc = input.charCodeAt(pos);
643 while (cc === CC_ASTERISK) {
644 pos++;
645 if (pos === input.length) return pos;
646 cc = input.charCodeAt(pos);
647 if (cc === CC_SLASH) {
648 pos++;
649 continue loop;
650 }
651 }
652 }
653 } else if (_isWhiteSpace(cc)) {
654 pos++;
655 continue;
656 }
657 return pos;
658 }
659};