source: frontend/node_modules/css-tree/lib/lexer/match.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 19.1 KB
RevLine 
[9af201e]1var hasOwnProperty = Object.prototype.hasOwnProperty;
2var matchGraph = require('./match-graph');
3var MATCH = matchGraph.MATCH;
4var MISMATCH = matchGraph.MISMATCH;
5var DISALLOW_EMPTY = matchGraph.DISALLOW_EMPTY;
6var TYPE = require('../tokenizer/const').TYPE;
7
8var STUB = 0;
9var TOKEN = 1;
10var OPEN_SYNTAX = 2;
11var CLOSE_SYNTAX = 3;
12
13var EXIT_REASON_MATCH = 'Match';
14var EXIT_REASON_MISMATCH = 'Mismatch';
15var EXIT_REASON_ITERATION_LIMIT = 'Maximum iteration number exceeded (please fill an issue on https://github.com/csstree/csstree/issues)';
16
17var ITERATION_LIMIT = 15000;
18var totalIterationCount = 0;
19
20function reverseList(list) {
21 var prev = null;
22 var next = null;
23 var item = list;
24
25 while (item !== null) {
26 next = item.prev;
27 item.prev = prev;
28 prev = item;
29 item = next;
30 }
31
32 return prev;
33}
34
35function areStringsEqualCaseInsensitive(testStr, referenceStr) {
36 if (testStr.length !== referenceStr.length) {
37 return false;
38 }
39
40 for (var i = 0; i < testStr.length; i++) {
41 var testCode = testStr.charCodeAt(i);
42 var referenceCode = referenceStr.charCodeAt(i);
43
44 // testCode.toLowerCase() for U+0041 LATIN CAPITAL LETTER A (A) .. U+005A LATIN CAPITAL LETTER Z (Z).
45 if (testCode >= 0x0041 && testCode <= 0x005A) {
46 testCode = testCode | 32;
47 }
48
49 if (testCode !== referenceCode) {
50 return false;
51 }
52 }
53
54 return true;
55}
56
57function isCommaContextStart(token) {
58 if (token === null) {
59 return true;
60 }
61
62 return (
63 token.type === TYPE.Comma ||
64 token.type === TYPE.Function ||
65 token.type === TYPE.LeftParenthesis ||
66 token.type === TYPE.LeftSquareBracket ||
67 token.type === TYPE.LeftCurlyBracket ||
68 token.type === TYPE.Delim
69 );
70}
71
72function isCommaContextEnd(token) {
73 if (token === null) {
74 return true;
75 }
76
77 return (
78 token.type === TYPE.RightParenthesis ||
79 token.type === TYPE.RightSquareBracket ||
80 token.type === TYPE.RightCurlyBracket ||
81 token.type === TYPE.Delim
82 );
83}
84
85function internalMatch(tokens, state, syntaxes) {
86 function moveToNextToken() {
87 do {
88 tokenIndex++;
89 token = tokenIndex < tokens.length ? tokens[tokenIndex] : null;
90 } while (token !== null && (token.type === TYPE.WhiteSpace || token.type === TYPE.Comment));
91 }
92
93 function getNextToken(offset) {
94 var nextIndex = tokenIndex + offset;
95
96 return nextIndex < tokens.length ? tokens[nextIndex] : null;
97 }
98
99 function stateSnapshotFromSyntax(nextState, prev) {
100 return {
101 nextState: nextState,
102 matchStack: matchStack,
103 syntaxStack: syntaxStack,
104 thenStack: thenStack,
105 tokenIndex: tokenIndex,
106 prev: prev
107 };
108 }
109
110 function pushThenStack(nextState) {
111 thenStack = {
112 nextState: nextState,
113 matchStack: matchStack,
114 syntaxStack: syntaxStack,
115 prev: thenStack
116 };
117 }
118
119 function pushElseStack(nextState) {
120 elseStack = stateSnapshotFromSyntax(nextState, elseStack);
121 }
122
123 function addTokenToMatch() {
124 matchStack = {
125 type: TOKEN,
126 syntax: state.syntax,
127 token: token,
128 prev: matchStack
129 };
130
131 moveToNextToken();
132 syntaxStash = null;
133
134 if (tokenIndex > longestMatch) {
135 longestMatch = tokenIndex;
136 }
137 }
138
139 function openSyntax() {
140 syntaxStack = {
141 syntax: state.syntax,
142 opts: state.syntax.opts || (syntaxStack !== null && syntaxStack.opts) || null,
143 prev: syntaxStack
144 };
145
146 matchStack = {
147 type: OPEN_SYNTAX,
148 syntax: state.syntax,
149 token: matchStack.token,
150 prev: matchStack
151 };
152 }
153
154 function closeSyntax() {
155 if (matchStack.type === OPEN_SYNTAX) {
156 matchStack = matchStack.prev;
157 } else {
158 matchStack = {
159 type: CLOSE_SYNTAX,
160 syntax: syntaxStack.syntax,
161 token: matchStack.token,
162 prev: matchStack
163 };
164 }
165
166 syntaxStack = syntaxStack.prev;
167 }
168
169 var syntaxStack = null;
170 var thenStack = null;
171 var elseStack = null;
172
173 // null – stashing allowed, nothing stashed
174 // false – stashing disabled, nothing stashed
175 // anithing else – fail stashable syntaxes, some syntax stashed
176 var syntaxStash = null;
177
178 var iterationCount = 0; // count iterations and prevent infinite loop
179 var exitReason = null;
180
181 var token = null;
182 var tokenIndex = -1;
183 var longestMatch = 0;
184 var matchStack = {
185 type: STUB,
186 syntax: null,
187 token: null,
188 prev: null
189 };
190
191 moveToNextToken();
192
193 while (exitReason === null && ++iterationCount < ITERATION_LIMIT) {
194 // function mapList(list, fn) {
195 // var result = [];
196 // while (list) {
197 // result.unshift(fn(list));
198 // list = list.prev;
199 // }
200 // return result;
201 // }
202 // console.log('--\n',
203 // '#' + iterationCount,
204 // require('util').inspect({
205 // match: mapList(matchStack, x => x.type === TOKEN ? x.token && x.token.value : x.syntax ? ({ [OPEN_SYNTAX]: '<', [CLOSE_SYNTAX]: '</' }[x.type] || x.type) + '!' + x.syntax.name : null),
206 // token: token && token.value,
207 // tokenIndex,
208 // syntax: syntax.type + (syntax.id ? ' #' + syntax.id : '')
209 // }, { depth: null })
210 // );
211 switch (state.type) {
212 case 'Match':
213 if (thenStack === null) {
214 // turn to MISMATCH when some tokens left unmatched
215 if (token !== null) {
216 // doesn't mismatch if just one token left and it's an IE hack
217 if (tokenIndex !== tokens.length - 1 || (token.value !== '\\0' && token.value !== '\\9')) {
218 state = MISMATCH;
219 break;
220 }
221 }
222
223 // break the main loop, return a result - MATCH
224 exitReason = EXIT_REASON_MATCH;
225 break;
226 }
227
228 // go to next syntax (`then` branch)
229 state = thenStack.nextState;
230
231 // check match is not empty
232 if (state === DISALLOW_EMPTY) {
233 if (thenStack.matchStack === matchStack) {
234 state = MISMATCH;
235 break;
236 } else {
237 state = MATCH;
238 }
239 }
240
241 // close syntax if needed
242 while (thenStack.syntaxStack !== syntaxStack) {
243 closeSyntax();
244 }
245
246 // pop stack
247 thenStack = thenStack.prev;
248 break;
249
250 case 'Mismatch':
251 // when some syntax is stashed
252 if (syntaxStash !== null && syntaxStash !== false) {
253 // there is no else branches or a branch reduce match stack
254 if (elseStack === null || tokenIndex > elseStack.tokenIndex) {
255 // restore state from the stash
256 elseStack = syntaxStash;
257 syntaxStash = false; // disable stashing
258 }
259 } else if (elseStack === null) {
260 // no else branches -> break the main loop
261 // return a result - MISMATCH
262 exitReason = EXIT_REASON_MISMATCH;
263 break;
264 }
265
266 // go to next syntax (`else` branch)
267 state = elseStack.nextState;
268
269 // restore all the rest stack states
270 thenStack = elseStack.thenStack;
271 syntaxStack = elseStack.syntaxStack;
272 matchStack = elseStack.matchStack;
273 tokenIndex = elseStack.tokenIndex;
274 token = tokenIndex < tokens.length ? tokens[tokenIndex] : null;
275
276 // pop stack
277 elseStack = elseStack.prev;
278 break;
279
280 case 'MatchGraph':
281 state = state.match;
282 break;
283
284 case 'If':
285 // IMPORTANT: else stack push must go first,
286 // since it stores the state of thenStack before changes
287 if (state.else !== MISMATCH) {
288 pushElseStack(state.else);
289 }
290
291 if (state.then !== MATCH) {
292 pushThenStack(state.then);
293 }
294
295 state = state.match;
296 break;
297
298 case 'MatchOnce':
299 state = {
300 type: 'MatchOnceBuffer',
301 syntax: state,
302 index: 0,
303 mask: 0
304 };
305 break;
306
307 case 'MatchOnceBuffer':
308 var terms = state.syntax.terms;
309
310 if (state.index === terms.length) {
311 // no matches at all or it's required all terms to be matched
312 if (state.mask === 0 || state.syntax.all) {
313 state = MISMATCH;
314 break;
315 }
316
317 // a partial match is ok
318 state = MATCH;
319 break;
320 }
321
322 // all terms are matched
323 if (state.mask === (1 << terms.length) - 1) {
324 state = MATCH;
325 break;
326 }
327
328 for (; state.index < terms.length; state.index++) {
329 var matchFlag = 1 << state.index;
330
331 if ((state.mask & matchFlag) === 0) {
332 // IMPORTANT: else stack push must go first,
333 // since it stores the state of thenStack before changes
334 pushElseStack(state);
335 pushThenStack({
336 type: 'AddMatchOnce',
337 syntax: state.syntax,
338 mask: state.mask | matchFlag
339 });
340
341 // match
342 state = terms[state.index++];
343 break;
344 }
345 }
346 break;
347
348 case 'AddMatchOnce':
349 state = {
350 type: 'MatchOnceBuffer',
351 syntax: state.syntax,
352 index: 0,
353 mask: state.mask
354 };
355 break;
356
357 case 'Enum':
358 if (token !== null) {
359 var name = token.value.toLowerCase();
360
361 // drop \0 and \9 hack from keyword name
362 if (name.indexOf('\\') !== -1) {
363 name = name.replace(/\\[09].*$/, '');
364 }
365
366 if (hasOwnProperty.call(state.map, name)) {
367 state = state.map[name];
368 break;
369 }
370 }
371
372 state = MISMATCH;
373 break;
374
375 case 'Generic':
376 var opts = syntaxStack !== null ? syntaxStack.opts : null;
377 var lastTokenIndex = tokenIndex + Math.floor(state.fn(token, getNextToken, opts));
378
379 if (!isNaN(lastTokenIndex) && lastTokenIndex > tokenIndex) {
380 while (tokenIndex < lastTokenIndex) {
381 addTokenToMatch();
382 }
383
384 state = MATCH;
385 } else {
386 state = MISMATCH;
387 }
388
389 break;
390
391 case 'Type':
392 case 'Property':
393 var syntaxDict = state.type === 'Type' ? 'types' : 'properties';
394 var dictSyntax = hasOwnProperty.call(syntaxes, syntaxDict) ? syntaxes[syntaxDict][state.name] : null;
395
396 if (!dictSyntax || !dictSyntax.match) {
397 throw new Error(
398 'Bad syntax reference: ' +
399 (state.type === 'Type'
400 ? '<' + state.name + '>'
401 : '<\'' + state.name + '\'>')
402 );
403 }
404
405 // stash a syntax for types with low priority
406 if (syntaxStash !== false && token !== null && state.type === 'Type') {
407 var lowPriorityMatching =
408 // https://drafts.csswg.org/css-values-4/#custom-idents
409 // When parsing positionally-ambiguous keywords in a property value, a <custom-ident> production
410 // can only claim the keyword if no other unfulfilled production can claim it.
411 (state.name === 'custom-ident' && token.type === TYPE.Ident) ||
412
413 // https://drafts.csswg.org/css-values-4/#lengths
414 // ... if a `0` could be parsed as either a <number> or a <length> in a property (such as line-height),
415 // it must parse as a <number>
416 (state.name === 'length' && token.value === '0');
417
418 if (lowPriorityMatching) {
419 if (syntaxStash === null) {
420 syntaxStash = stateSnapshotFromSyntax(state, elseStack);
421 }
422
423 state = MISMATCH;
424 break;
425 }
426 }
427
428 openSyntax();
429 state = dictSyntax.match;
430 break;
431
432 case 'Keyword':
433 var name = state.name;
434
435 if (token !== null) {
436 var keywordName = token.value;
437
438 // drop \0 and \9 hack from keyword name
439 if (keywordName.indexOf('\\') !== -1) {
440 keywordName = keywordName.replace(/\\[09].*$/, '');
441 }
442
443 if (areStringsEqualCaseInsensitive(keywordName, name)) {
444 addTokenToMatch();
445 state = MATCH;
446 break;
447 }
448 }
449
450 state = MISMATCH;
451 break;
452
453 case 'AtKeyword':
454 case 'Function':
455 if (token !== null && areStringsEqualCaseInsensitive(token.value, state.name)) {
456 addTokenToMatch();
457 state = MATCH;
458 break;
459 }
460
461 state = MISMATCH;
462 break;
463
464 case 'Token':
465 if (token !== null && token.value === state.value) {
466 addTokenToMatch();
467 state = MATCH;
468 break;
469 }
470
471 state = MISMATCH;
472 break;
473
474 case 'Comma':
475 if (token !== null && token.type === TYPE.Comma) {
476 if (isCommaContextStart(matchStack.token)) {
477 state = MISMATCH;
478 } else {
479 addTokenToMatch();
480 state = isCommaContextEnd(token) ? MISMATCH : MATCH;
481 }
482 } else {
483 state = isCommaContextStart(matchStack.token) || isCommaContextEnd(token) ? MATCH : MISMATCH;
484 }
485
486 break;
487
488 case 'String':
489 var string = '';
490
491 for (var lastTokenIndex = tokenIndex; lastTokenIndex < tokens.length && string.length < state.value.length; lastTokenIndex++) {
492 string += tokens[lastTokenIndex].value;
493 }
494
495 if (areStringsEqualCaseInsensitive(string, state.value)) {
496 while (tokenIndex < lastTokenIndex) {
497 addTokenToMatch();
498 }
499
500 state = MATCH;
501 } else {
502 state = MISMATCH;
503 }
504
505 break;
506
507 default:
508 throw new Error('Unknown node type: ' + state.type);
509 }
510 }
511
512 totalIterationCount += iterationCount;
513
514 switch (exitReason) {
515 case null:
516 console.warn('[csstree-match] BREAK after ' + ITERATION_LIMIT + ' iterations');
517 exitReason = EXIT_REASON_ITERATION_LIMIT;
518 matchStack = null;
519 break;
520
521 case EXIT_REASON_MATCH:
522 while (syntaxStack !== null) {
523 closeSyntax();
524 }
525 break;
526
527 default:
528 matchStack = null;
529 }
530
531 return {
532 tokens: tokens,
533 reason: exitReason,
534 iterations: iterationCount,
535 match: matchStack,
536 longestMatch: longestMatch
537 };
538}
539
540function matchAsList(tokens, matchGraph, syntaxes) {
541 var matchResult = internalMatch(tokens, matchGraph, syntaxes || {});
542
543 if (matchResult.match !== null) {
544 var item = reverseList(matchResult.match).prev;
545
546 matchResult.match = [];
547
548 while (item !== null) {
549 switch (item.type) {
550 case STUB:
551 break;
552
553 case OPEN_SYNTAX:
554 case CLOSE_SYNTAX:
555 matchResult.match.push({
556 type: item.type,
557 syntax: item.syntax
558 });
559 break;
560
561 default:
562 matchResult.match.push({
563 token: item.token.value,
564 node: item.token.node
565 });
566 break;
567 }
568
569 item = item.prev;
570 }
571 }
572
573 return matchResult;
574}
575
576function matchAsTree(tokens, matchGraph, syntaxes) {
577 var matchResult = internalMatch(tokens, matchGraph, syntaxes || {});
578
579 if (matchResult.match === null) {
580 return matchResult;
581 }
582
583 var item = matchResult.match;
584 var host = matchResult.match = {
585 syntax: matchGraph.syntax || null,
586 match: []
587 };
588 var hostStack = [host];
589
590 // revert a list and start with 2nd item since 1st is a stub item
591 item = reverseList(item).prev;
592
593 // build a tree
594 while (item !== null) {
595 switch (item.type) {
596 case OPEN_SYNTAX:
597 host.match.push(host = {
598 syntax: item.syntax,
599 match: []
600 });
601 hostStack.push(host);
602 break;
603
604 case CLOSE_SYNTAX:
605 hostStack.pop();
606 host = hostStack[hostStack.length - 1];
607 break;
608
609 default:
610 host.match.push({
611 syntax: item.syntax || null,
612 token: item.token.value,
613 node: item.token.node
614 });
615 }
616
617 item = item.prev;
618 }
619
620 return matchResult;
621}
622
623module.exports = {
624 matchAsList: matchAsList,
625 matchAsTree: matchAsTree,
626 getTotalIterationCount: function() {
627 return totalIterationCount;
628 }
629};
Note: See TracBrowser for help on using the repository browser.