UNPKG

19.6 kBJavaScriptView Raw
1/*
2Copyright (c) 2013 Dulin Marat
3
4Permission is hereby granted, free of charge, to any person obtaining a copy
5of this software and associated documentation files (the "Software"), to deal
6in the Software without restriction, including without limitation the rights
7to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8copies of the Software, and to permit persons to whom the Software is
9furnished to do so, subject to the following conditions:
10
11The above copyright notice and this permission notice shall be included in
12all copies or substantial portions of the Software.
13
14THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20THE SOFTWARE.
21*/
22function CssSelectorParser() {
23 this.pseudos = {};
24 this.attrEqualityMods = {};
25 this.ruleNestingOperators = {};
26 this.substitutesEnabled = false;
27}
28
29CssSelectorParser.prototype.registerSelectorPseudos = function(name) {
30 for (var j = 0, len = arguments.length; j < len; j++) {
31 name = arguments[j];
32 this.pseudos[name] = 'selector';
33 }
34 return this;
35};
36
37CssSelectorParser.prototype.unregisterSelectorPseudos = function(name) {
38 for (var j = 0, len = arguments.length; j < len; j++) {
39 name = arguments[j];
40 delete this.pseudos[name];
41 }
42 return this;
43};
44
45CssSelectorParser.prototype.registerNumericPseudos = function(name) {
46 for (var j = 0, len = arguments.length; j < len; j++) {
47 name = arguments[j];
48 this.pseudos[name] = 'numeric';
49 }
50 return this;
51};
52
53CssSelectorParser.prototype.unregisterNumericPseudos = function(name) {
54 for (var j = 0, len = arguments.length; j < len; j++) {
55 name = arguments[j];
56 delete this.pseudos[name];
57 }
58 return this;
59};
60
61CssSelectorParser.prototype.registerNestingOperators = function(operator) {
62 for (var j = 0, len = arguments.length; j < len; j++) {
63 operator = arguments[j];
64 this.ruleNestingOperators[operator] = true;
65 }
66 return this;
67};
68
69CssSelectorParser.prototype.unregisterNestingOperators = function(operator) {
70 for (var j = 0, len = arguments.length; j < len; j++) {
71 operator = arguments[j];
72 delete this.ruleNestingOperators[operator];
73 }
74 return this;
75};
76
77CssSelectorParser.prototype.registerAttrEqualityMods = function(mod) {
78 for (var j = 0, len = arguments.length; j < len; j++) {
79 mod = arguments[j];
80 this.attrEqualityMods[mod] = true;
81 }
82 return this;
83};
84
85CssSelectorParser.prototype.unregisterAttrEqualityMods = function(mod) {
86 for (var j = 0, len = arguments.length; j < len; j++) {
87 mod = arguments[j];
88 delete this.attrEqualityMods[mod];
89 }
90 return this;
91};
92
93CssSelectorParser.prototype.enableSubstitutes = function() {
94 this.substitutesEnabled = true;
95 return this;
96};
97
98CssSelectorParser.prototype.disableSubstitutes = function() {
99 this.substitutesEnabled = false;
100 return this;
101};
102
103function isIdentStart(c) {
104 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c === '-') || (c === '_');
105}
106
107function isIdent(c) {
108 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '-' || c === '_';
109}
110
111function isHex(c) {
112 return (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || (c >= '0' && c <= '9');
113}
114
115function isDecimal(c) {
116 return c >= '0' && c <= '9';
117}
118
119function isAttrMatchOperator(chr) {
120 return chr === '=' || chr === '^' || chr === '$' || chr === '*' || chr === '~';
121}
122
123var identSpecialChars = {
124 '!': true,
125 '"': true,
126 '#': true,
127 '$': true,
128 '%': true,
129 '&': true,
130 '\'': true,
131 '(': true,
132 ')': true,
133 '*': true,
134 '+': true,
135 ',': true,
136 '.': true,
137 '/': true,
138 ';': true,
139 '<': true,
140 '=': true,
141 '>': true,
142 '?': true,
143 '@': true,
144 '[': true,
145 '\\': true,
146 ']': true,
147 '^': true,
148 '`': true,
149 '{': true,
150 '|': true,
151 '}': true,
152 '~': true
153};
154
155var strReplacementsRev = {
156 '\n': '\\n',
157 '\r': '\\r',
158 '\t': '\\t',
159 '\f': '\\f',
160 '\v': '\\v'
161};
162
163var singleQuoteEscapeChars = {
164 n: '\n',
165 r: '\r',
166 t: '\t',
167 f: '\f',
168 '\\': '\\',
169 '\'': '\''
170};
171
172var doubleQuotesEscapeChars = {
173 n: '\n',
174 r: '\r',
175 t: '\t',
176 f: '\f',
177 '\\': '\\',
178 '"': '"'
179};
180
181function ParseContext(str, pos, pseudos, attrEqualityMods, ruleNestingOperators, substitutesEnabled) {
182 var chr, getIdent, getStr, l, skipWhitespace;
183 l = str.length;
184 chr = null;
185 getStr = function(quote, escapeTable) {
186 var esc, hex, result;
187 result = '';
188 pos++;
189 chr = str.charAt(pos);
190 while (pos < l) {
191 if (chr === quote) {
192 pos++;
193 return result;
194 } else if (chr === '\\') {
195 pos++;
196 chr = str.charAt(pos);
197 if (chr === quote) {
198 result += quote;
199 } else if (esc = escapeTable[chr]) {
200 result += esc;
201 } else if (isHex(chr)) {
202 hex = chr;
203 pos++;
204 chr = str.charAt(pos);
205 while (isHex(chr)) {
206 hex += chr;
207 pos++;
208 chr = str.charAt(pos);
209 }
210 if (chr === ' ') {
211 pos++;
212 chr = str.charAt(pos);
213 }
214 result += String.fromCharCode(parseInt(hex, 16));
215 continue;
216 } else {
217 result += chr;
218 }
219 } else {
220 result += chr;
221 }
222 pos++;
223 chr = str.charAt(pos);
224 }
225 return result;
226 };
227 getIdent = function(specials) {
228 var result = '';
229 chr = str.charAt(pos);
230 while (pos < l) {
231 if (isIdent(chr) || (specials && specials[chr])) {
232 result += chr;
233 } else if (chr === '\\') {
234 pos++;
235 if (pos >= l) {
236 throw Error('Expected symbol but end of file reached.');
237 }
238 chr = str.charAt(pos);
239 if (identSpecialChars[chr]) {
240 result += chr;
241 } else if (isHex(chr)) {
242 var hex = chr;
243 pos++;
244 chr = str.charAt(pos);
245 while (isHex(chr)) {
246 hex += chr;
247 pos++;
248 chr = str.charAt(pos);
249 }
250 if (chr === ' ') {
251 pos++;
252 chr = str.charAt(pos);
253 }
254 result += String.fromCharCode(parseInt(hex, 16));
255 continue;
256 } else {
257 result += chr;
258 }
259 } else {
260 return result;
261 }
262 pos++;
263 chr = str.charAt(pos);
264 }
265 return result;
266 };
267 skipWhitespace = function() {
268 chr = str.charAt(pos);
269 var result = false;
270 while (chr === ' ' || chr === "\t" || chr === "\n" || chr === "\r" || chr === "\f") {
271 result = true;
272 pos++;
273 chr = str.charAt(pos);
274 }
275 return result;
276 };
277 this.parse = function() {
278 var res = this.parseSelector();
279 if (pos < l) {
280 throw Error('Rule expected but "' + str.charAt(pos) + '" found.');
281 }
282 return res;
283 };
284 this.parseSelector = function() {
285 var res;
286 var selector = res = this.parseSingleSelector();
287 chr = str.charAt(pos);
288 while (chr === ',') {
289 pos++;
290 skipWhitespace();
291 if (res.type !== 'selectors') {
292 res = {
293 type: 'selectors',
294 selectors: [selector]
295 };
296 }
297 selector = this.parseSingleSelector();
298 if (!selector) {
299 throw Error('Rule expected after ",".');
300 }
301 res.selectors.push(selector);
302 }
303 return res;
304 };
305
306 this.parseSingleSelector = function() {
307 skipWhitespace();
308
309 let opm = str.slice(pos,pos + 4).match(/^(\>{1,3}|\+|~)/);
310
311 var selector = {
312 type: 'ruleSet'
313 };
314
315 var rule = opm ? {type: 'rule', isScope: true} : this.parseRule();
316
317 if (!rule) {
318 return null;
319 }
320 var currentRule = selector;
321 while (rule) {
322 rule.type = 'rule';
323 currentRule.rule = rule;
324 currentRule = rule;
325 skipWhitespace();
326 chr = str.charAt(pos);
327 if (pos >= l || chr === ',' || chr === ')') {
328 break;
329 }
330 if (ruleNestingOperators[chr]) {
331 var op = chr;
332 if(op == '>' && str.charAt(pos + 1) == '>' && str.charAt(pos + 2) == '>'){
333 op = '>>>';
334 pos = pos + 3;
335 } else if(op == '>' && str.charAt(pos + 1) == '>'){
336 op = '>>';
337 pos = pos + 2;
338 } else {
339 pos++;
340 }
341 skipWhitespace();
342 rule = this.parseRule();
343
344 if (!rule) {
345 if(op == '>' || op == '>>>' || op == '>>'){
346 rule = {tagName: '*'}
347 } else {
348 throw Error('Rule expected after "' + op + '".');
349 }
350 }
351 rule.nestingOperator = op;
352 } else {
353 rule = this.parseRule();
354 if (rule) {
355 rule.nestingOperator = null;
356 }
357 }
358 }
359 return selector;
360 };
361
362 this.parseRule = function() {
363 var rule = null;
364 while (pos < l) {
365 chr = str.charAt(pos);
366 if (chr === '&') {
367 pos++;
368 (rule = rule || {}).isScope = true;
369 } else if (chr === '*') {
370 pos++;
371 (rule = rule || {}).tagName = '*';
372 } else if (isIdentStart(chr) || chr === '\\') {
373 (rule = rule || {}).tagName = getIdent();
374 } else if (chr === '$' || chr === '%') {
375 pos++;
376 rule = rule || {};
377 (rule.classNames = rule.classNames || []).push(chr + getIdent());
378 } else if (chr === '.') {
379 pos++;
380 rule = rule || {};
381 (rule.classNames = rule.classNames || []).push(getIdent());
382 } else if (chr === '#') {
383 pos++;
384 (rule = rule || {}).id = getIdent();
385 } else if (chr === '[') {
386 pos++;
387 skipWhitespace();
388 var attr = {
389 name: getIdent()
390 };
391 skipWhitespace();
392 if (chr === ']') {
393 pos++;
394 } else {
395 var operator = '';
396 if (attrEqualityMods[chr]) {
397 operator = chr;
398 pos++;
399 chr = str.charAt(pos);
400 }
401 if (pos >= l) {
402 throw Error('Expected "=" but end of file reached.');
403 }
404 if (chr !== '=') {
405 throw Error('Expected "=" but "' + chr + '" found.');
406 }
407 attr.operator = operator + '=';
408 pos++;
409 skipWhitespace();
410 var attrValue = '';
411 attr.valueType = 'string';
412 if (chr === '"') {
413 attrValue = getStr('"', doubleQuotesEscapeChars);
414 } else if (chr === '\'') {
415 attrValue = getStr('\'', singleQuoteEscapeChars);
416 } else if (substitutesEnabled && chr === '$') {
417 pos++;
418 attrValue = getIdent();
419 attr.valueType = 'substitute';
420 } else {
421 while (pos < l) {
422 if (chr === ']') {
423 break;
424 }
425 attrValue += chr;
426 pos++;
427 chr = str.charAt(pos);
428 }
429 attrValue = attrValue.trim();
430 }
431 skipWhitespace();
432 if (pos >= l) {
433 throw Error('Expected "]" but end of file reached.');
434 }
435 if (chr !== ']') {
436 throw Error('Expected "]" but "' + chr + '" found.');
437 }
438 pos++;
439 attr.value = attrValue;
440 }
441 rule = rule || {};
442 (rule.attrs = rule.attrs || []).push(attr);
443 } else if (chr === ':' || chr === '@') {
444 let special = chr === '@';
445
446 pos++;
447
448 var pseudoName = ''
449 while(str.charAt(pos) == '.'){
450 pseudoName += '.';
451 pos++;
452 }
453
454
455 pseudoName += getIdent({'~':true,'+':true,'.':false,'>':true,'<':true,'!':true});
456 var pseudo = {
457 special: special,
458 name: pseudoName
459 };
460 if (chr === '(') {
461 pos++;
462 var value = '';
463 skipWhitespace();
464 if (pseudos[pseudoName] === 'selector') {
465 pseudo.valueType = 'selector';
466 value = this.parseSelector();
467 } else {
468 pseudo.valueType = pseudos[pseudoName] || 'string';
469 if (chr === '"') {
470 value = getStr('"', doubleQuotesEscapeChars);
471 } else if (chr === '\'') {
472 value = getStr('\'', singleQuoteEscapeChars);
473 } else if (substitutesEnabled && chr === '$') {
474 pos++;
475 value = getIdent();
476 pseudo.valueType = 'substitute';
477 } else {
478 while (pos < l) {
479 if (chr === ')') {
480 break;
481 }
482 value += chr;
483 pos++;
484 chr = str.charAt(pos);
485 }
486 value = value.trim();
487 }
488 skipWhitespace();
489 }
490 if (pos >= l) {
491 throw Error('Expected ")" but end of file reached.');
492 }
493 if (chr !== ')') {
494 throw Error('Expected ")" but "' + chr + '" found.');
495 }
496 pos++;
497 pseudo.value = value;
498 }
499 rule = rule || {};
500 (rule.pseudos = rule.pseudos || []).push(pseudo);
501 } else {
502 break;
503 }
504 }
505 return rule;
506 };
507 return this;
508}
509
510CssSelectorParser.prototype.parse = function(str) {
511 var context = new ParseContext(
512 str,
513 0,
514 this.pseudos,
515 this.attrEqualityMods,
516 this.ruleNestingOperators,
517 this.substitutesEnabled
518 );
519 return context.parse();
520};
521
522CssSelectorParser.prototype.escapeIdentifier = function(s) {
523 var result = '';
524 var i = 0;
525 var len = s.length;
526 while (i < len) {
527 var chr = s.charAt(i);
528 if (identSpecialChars[chr]) {
529 result += '\\' + chr;
530 } else {
531 if (
532 !(
533 chr === '_' || chr === '-' ||
534 (chr >= 'A' && chr <= 'Z') ||
535 (chr >= 'a' && chr <= 'z') ||
536 (i !== 0 && chr >= '0' && chr <= '9')
537 )
538 ) {
539 var charCode = chr.charCodeAt(0);
540 if ((charCode & 0xF800) === 0xD800) {
541 var extraCharCode = s.charCodeAt(i++);
542 if ((charCode & 0xFC00) !== 0xD800 || (extraCharCode & 0xFC00) !== 0xDC00) {
543 throw Error('UCS-2(decode): illegal sequence');
544 }
545 charCode = ((charCode & 0x3FF) << 10) + (extraCharCode & 0x3FF) + 0x10000;
546 }
547 result += '\\' + charCode.toString(16) + ' ';
548 } else {
549 result += chr;
550 }
551 }
552 i++;
553 }
554 return result;
555};
556
557CssSelectorParser.prototype.escapeStr = function(s) {
558 var result = '';
559 var i = 0;
560 var len = s.length;
561 var chr, replacement;
562 while (i < len) {
563 chr = s.charAt(i);
564 if (chr === '"') {
565 chr = '\\"';
566 } else if (chr === '\\') {
567 chr = '\\\\';
568 } else if (replacement = strReplacementsRev[chr]) {
569 chr = replacement;
570 }
571 result += chr;
572 i++;
573 }
574 return "\"" + result + "\"";
575};
576
577CssSelectorParser.prototype.render = function(path) {
578 return this._renderEntity(path).trim();
579};
580
581CssSelectorParser.prototype._renderEntity = function(entity) {
582 var currentEntity, parts, res;
583 res = '';
584 switch (entity.type) {
585 case 'ruleSet':
586 currentEntity = entity.rule;
587 parts = [];
588 while (currentEntity) {
589 if (currentEntity.nestingOperator) {
590 parts.push(currentEntity.nestingOperator);
591 }
592 parts.push(this._renderEntity(currentEntity));
593 currentEntity = currentEntity.rule;
594 }
595 res = parts.join(' ');
596 break;
597 case 'selectors':
598 res = entity.selectors.map(this._renderEntity, this).join(', ');
599 break;
600 case 'rule':
601 let s0 = entity.s1;
602 let s1 = entity.s2;
603
604 if (entity.tagName) {
605 if (entity.tagName === '*') {
606 res = '*';
607 } else {
608 res = this.escapeIdentifier(entity.tagName);
609 }
610 }
611 if (entity.id) {
612 res += "#" + this.escapeIdentifier(entity.id);
613 }
614 if (entity.classNames) {
615 let shortest = null;
616
617 res += entity.classNames.map(function(cn) {
618 if(cn[0] == '!') {
619 return ":not(." + this.escapeIdentifier(cn.slice(1)) + ")";
620 } else {
621 let str = this.escapeIdentifier(cn);
622 if(s1 && (!shortest || shortest.length > str.length)){
623 shortest = str;
624 }
625 return "." + str;
626 }
627 }, this).join('');
628
629 if(s1 > 0 && shortest && shortest.length < 9){
630 while(--s1 >= 0){
631 res += "." + shortest;
632 }
633 }
634 }
635
636 if(entity.pri > 0 && false){
637 let i = entity.pri;
638 // res += ":not(";
639 // while (--i >= 0) res += '#_';
640 // res += ')';
641 while (--i >= 0) res += ":not(#P)";
642 }
643 if(s0 > 0){
644 if(false){
645 res += ":not(";
646 while (s0--) res += '#_';
647 while (--s1 >= 0) res += '._';
648 res += ')';
649 } else {
650 while (--s0 >= 0) res += ":not(#_)";
651 }
652 // while (--i >= 0) res += ":not(#_)";
653 }
654 if(s1 > 0){
655 while (--s0 >= 0) res += ":not(._0)";
656 // res += ":not(";
657 // while (--s1 >= 0) res += (s1 ? '._' : '._0');
658 // res += ')';
659 // while (--i >= 0) res += ":not(._)";
660 }
661 if (entity.attrs) {
662 res += entity.attrs.map(function(attr) {
663 if (attr.operator) {
664 if (attr.valueType === 'substitute') {
665 return "[" + this.escapeIdentifier(attr.name) + attr.operator + "$" + attr.value + "]";
666 } else {
667 return "[" + this.escapeIdentifier(attr.name) + attr.operator + this.escapeStr(attr.value) + "]";
668 }
669 } else {
670 return "[" + this.escapeIdentifier(attr.name) + "]";
671 }
672 }, this).join('');
673 }
674 if (entity.pseudos) {
675 res += entity.pseudos.map(function(pseudo) {
676 let pre = ":" + this.escapeIdentifier(pseudo.name);
677 let post = "";
678
679 if(pseudo.neg){
680 pre = ":not(" + pre;
681 post = ")";
682
683 }
684
685 if (pseudo.valueType) {
686 if (pseudo.valueType === 'selector') {
687 return pre + "(" + this._renderEntity(pseudo.value) + ")" + post;
688 } else if (pseudo.valueType === 'substitute') {
689 return pre + "($" + pseudo.value + ")" + post;
690 } else if (pseudo.valueType === 'numeric') {
691 return pre + "(" + pseudo.value + ")" + post;
692 } else if (pseudo.valueType === 'raw' || pseudo.valueType === 'string' ) {
693 return pre + "(" + pseudo.value + ")" + post;
694 } else {
695 return pre + "(" + this.escapeIdentifier(pseudo.value) + ")" + post;
696 }
697 } else if(pseudo.type == 'el') {
698 return ':' + pre;
699 } else {
700 return pre + post;
701 }
702 }, this).join('');
703 }
704 break;
705 default:
706 throw Error('Unknown entity type: "' + entity.type(+'".'));
707 }
708 return res;
709};
710
711var parser = new CssSelectorParser();
712parser.registerSelectorPseudos('has','not','is','matches','any')
713parser.registerNumericPseudos('nth-child')
714parser.registerNestingOperators('>>>','>>','>', '+', '~')
715parser.registerAttrEqualityMods('^', '$', '*', '~')
716// parser.enableSubstitutes()
717
718export const parse = function(v){ return parser.parse(v) }
719export const render = function(v){ return parser.render(v) }
720// exports.default = parser;
\No newline at end of file