@asamuzakjp/dom-selector 0.4.2 → 0.5.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/package.json CHANGED
@@ -26,7 +26,7 @@
26
26
  "chai": "^4.3.7",
27
27
  "eslint": "^8.40.0",
28
28
  "eslint-config-standard": "^17.0.0",
29
- "eslint-plugin-jsdoc": "^44.2.0",
29
+ "eslint-plugin-jsdoc": "^44.2.3",
30
30
  "eslint-plugin-regexp": "^1.15.0",
31
31
  "eslint-plugin-unicorn": "^47.0.0",
32
32
  "jsdom": "^22.0.0",
@@ -40,5 +40,5 @@
40
40
  "test": "c8 --reporter=text mocha --exit test/**/*.test.js",
41
41
  "tsc": "npx tsc"
42
42
  },
43
- "version": "0.4.2"
43
+ "version": "0.5.0"
44
44
  }
package/src/js/matcher.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  /* import */
6
- const { parseSelector, walkAst } = require('./parser.js');
6
+ const { generateCSS, parseSelector, walkAST } = require('./parser.js');
7
7
 
8
8
  /* constants */
9
9
  const {
@@ -11,34 +11,59 @@ const {
11
11
  NTH, PSEUDO_CLASS_SELECTOR, TYPE_SELECTOR
12
12
  } = require('./constant.js');
13
13
  const ELEMENT_NODE = 1;
14
- const REG_PSEUDO_FUNC = /^(?:(?:ha|i)s|not|where)$/;
15
- const REG_PSEUDO_NTH = /^nth-(?:last-)?(?:child|of-type)$/;
14
+ // FIXME: custom element name is not fully implemented
15
+ // @see https://html.spec.whatwg.org/#valid-custom-element-name
16
+ const HTML_CUSTOM_ELEMENT = /^[a-z][\d._a-z]*-[\d\-._a-z]*$/;
17
+ const HTML_FORM_INPUT = /^(?:(?:inpu|selec)t|textarea)$/;
18
+ const HTML_FORM_PARTS = /^(?:button|fieldset|opt(?:group|ion))$/;
19
+ const HTML_INTERACT = /^d(?:etails|ialog)$/;
20
+ const PSEUDO_FUNC = /^(?:(?:ha|i)s|not|where)$/;
21
+ const PSEUDO_NTH = /^nth-(?:last-)?(?:child|of-type)$/;
16
22
 
17
23
  /**
18
24
  * collect nth child
25
+ * @param {object} anb - An+B options
26
+ * @param {number} anb.a - a
27
+ * @param {number} anb.b - b
28
+ * @param {boolean} [anb.reverse] - reverse order
29
+ * @param {string} [anb.selector] - CSS selector
19
30
  * @param {object} node - Element node
20
- * @param {object} opt - options
21
- * @param {number} opt.a - a
22
- * @param {number} opt.b - b
23
- * @param {boolean} [opt.reverse] - reverse order
24
31
  * @returns {Array.<object|undefined>} - collection of matched nodes
25
32
  */
26
- const collectNthChild = (node = {}, opt = {}) => {
27
- const { nodeType, parentNode } = node;
28
- const { a, b, reverse } = opt;
29
- const res = new Set();
30
- if (nodeType === ELEMENT_NODE &&
31
- Number.isInteger(a) && Number.isInteger(b)) {
33
+ const collectNthChild = (anb = {}, node = {}) => {
34
+ const { a, b, reverse, selector } = anb;
35
+ const { nodeType, ownerDocument, parentNode } = node;
36
+ const matched = [];
37
+ if (Number.isInteger(a) && Number.isInteger(b) && nodeType === ELEMENT_NODE) {
32
38
  const arr = [...parentNode.children];
33
39
  if (reverse) {
34
40
  arr.reverse();
35
41
  }
36
42
  const l = arr.length;
37
- // :first-child, :last-child
43
+ const items = [];
44
+ if (selector) {
45
+ const a = new Matcher(selector, ownerDocument).querySelectorAll();
46
+ if (a.length) {
47
+ items.push(...a);
48
+ }
49
+ }
50
+ // :first-child, :last-child, :nth-child(0 of S)
38
51
  if (a === 0) {
39
52
  if (b >= 0 && b < l) {
40
- const item = arr[b];
41
- res.add(item);
53
+ if (items.length) {
54
+ let i = 0;
55
+ while (i < l) {
56
+ const current = arr[i];
57
+ if (items.includes(current)) {
58
+ matched.push(current);
59
+ break;
60
+ }
61
+ i++;
62
+ }
63
+ } else {
64
+ const current = arr[b];
65
+ matched.push(current);
66
+ }
42
67
  }
43
68
  // :nth-child()
44
69
  } else {
@@ -49,12 +74,25 @@ const collectNthChild = (node = {}, opt = {}) => {
49
74
  nth += (++n * a);
50
75
  }
51
76
  }
52
- if (nth >= 0) {
77
+ if (nth >= 0 && nth < l) {
53
78
  let i = 0;
54
- while (i < l && nth < l) {
55
- if (i === nth) {
56
- const item = arr[i];
57
- res.add(item);
79
+ let j = a > 0 ? 0 : b - 1;
80
+ while (i < l && nth >= 0 && nth < l) {
81
+ const current = arr[i];
82
+ if (items.length) {
83
+ if (items.includes(current)) {
84
+ if (j === nth) {
85
+ matched.push(current);
86
+ nth += a;
87
+ }
88
+ if (a > 0) {
89
+ j++;
90
+ } else {
91
+ j--;
92
+ }
93
+ }
94
+ } else if (i === nth) {
95
+ matched.push(current);
58
96
  nth += a;
59
97
  }
60
98
  i++;
@@ -62,24 +100,23 @@ const collectNthChild = (node = {}, opt = {}) => {
62
100
  }
63
101
  }
64
102
  }
65
- return [...res];
103
+ return [...new Set(matched)];
66
104
  };
67
105
 
68
106
  /**
69
107
  * collect nth of type
108
+ * @param {object} anb - An+B options
109
+ * @param {number} anb.a - a
110
+ * @param {number} anb.b - b
111
+ * @param {boolean} [anb.reverse] - reverse order
70
112
  * @param {object} node - Element node
71
- * @param {object} opt - options
72
- * @param {number} opt.a - a
73
- * @param {number} opt.b - b
74
- * @param {boolean} [opt.reverse] - reverse order
75
113
  * @returns {Array.<object|undefined>} - collection of matched nodes
76
114
  */
77
- const collectNthOfType = (node = {}, opt = {}) => {
115
+ const collectNthOfType = (anb = {}, node = {}) => {
116
+ const { a, b, reverse } = anb;
78
117
  const { localName, nodeType, parentNode, prefix } = node;
79
- const { a, b, reverse } = opt;
80
- const res = new Set();
81
- if (nodeType === ELEMENT_NODE &&
82
- Number.isInteger(a) && Number.isInteger(b)) {
118
+ const matched = [];
119
+ if (Number.isInteger(a) && Number.isInteger(b) && nodeType === ELEMENT_NODE) {
83
120
  const arr = [...parentNode.children];
84
121
  if (reverse) {
85
122
  arr.reverse();
@@ -91,11 +128,11 @@ const collectNthOfType = (node = {}, opt = {}) => {
91
128
  let i = 0;
92
129
  let j = 0;
93
130
  while (i < l) {
94
- const item = arr[i];
95
- const { localName: itemLocalName, prefix: itemPrefix } = item;
131
+ const current = arr[i];
132
+ const { localName: itemLocalName, prefix: itemPrefix } = current;
96
133
  if (itemLocalName === localName && itemPrefix === prefix) {
97
134
  if (j === b) {
98
- res.add(item);
135
+ matched.push(current);
99
136
  break;
100
137
  }
101
138
  j++;
@@ -111,25 +148,104 @@ const collectNthOfType = (node = {}, opt = {}) => {
111
148
  nth += a;
112
149
  }
113
150
  }
114
- if (nth >= 0) {
151
+ if (nth >= 0 && nth < l) {
115
152
  let i = 0;
116
- let j = 0;
117
- while (i < l && nth < l) {
118
- const item = arr[i];
119
- const { localName: itemLocalName, prefix: itemPrefix } = item;
153
+ let j = a > 0 ? 0 : b - 1;
154
+ while (i < l && nth >= 0 && nth < l) {
155
+ const current = arr[i];
156
+ const { localName: itemLocalName, prefix: itemPrefix } = current;
120
157
  if (itemLocalName === localName && itemPrefix === prefix) {
121
158
  if (j === nth) {
122
- res.add(item);
159
+ matched.push(current);
123
160
  nth += a;
124
161
  }
125
- j++;
162
+ if (a > 0) {
163
+ j++;
164
+ } else {
165
+ j--;
166
+ }
126
167
  }
127
168
  i++;
128
169
  }
129
170
  }
130
171
  }
131
172
  }
132
- return [...res];
173
+ return [...new Set(matched)];
174
+ };
175
+
176
+ /**
177
+ * match An+B
178
+ * @param {string} nthName - nth pseudo-class name
179
+ * @param {object} ast - AST
180
+ * @param {object} node - Element node
181
+ * @returns {Array.<object|undefined>} - collection of matched nodes
182
+ */
183
+ const matchAnPlusB = (nthName, ast = {}, node = {}) => {
184
+ const matched = [];
185
+ if (typeof nthName === 'string') {
186
+ nthName = nthName.trim();
187
+ if (PSEUDO_NTH.test(nthName)) {
188
+ const {
189
+ nth: {
190
+ a,
191
+ b,
192
+ name: identName
193
+ },
194
+ selector: astSelector,
195
+ type: astType
196
+ } = ast;
197
+ const { nodeType } = node;
198
+ if (astType === NTH && nodeType === ELEMENT_NODE) {
199
+ const anbMap = new Map();
200
+ if (identName) {
201
+ if (identName === 'even') {
202
+ anbMap.set('a', 2);
203
+ anbMap.set('b', 0);
204
+ } else if (identName === 'odd') {
205
+ anbMap.set('a', 2);
206
+ anbMap.set('b', 1);
207
+ }
208
+ if (/last/.test(nthName)) {
209
+ anbMap.set('reverse', true);
210
+ }
211
+ } else {
212
+ if (typeof a === 'string' && /-?\d+/.test(a)) {
213
+ anbMap.set('a', a * 1);
214
+ } else {
215
+ anbMap.set('a', 0);
216
+ }
217
+ if (typeof b === 'string' && /-?\d+/.test(b)) {
218
+ anbMap.set('b', b * 1);
219
+ } else {
220
+ anbMap.set('b', 0);
221
+ }
222
+ if (/last/.test(nthName)) {
223
+ anbMap.set('reverse', true);
224
+ }
225
+ }
226
+ if (anbMap.has('a') && anbMap.has('b')) {
227
+ if (/^nth-(?:last-)?child$/.test(nthName)) {
228
+ if (astSelector) {
229
+ const css = generateCSS(astSelector);
230
+ anbMap.set('selector', css);
231
+ }
232
+ const anb = Object.fromEntries(anbMap);
233
+ const arr = collectNthChild(anb, node);
234
+ if (arr.length) {
235
+ matched.push(...arr);
236
+ }
237
+ } else if (/^nth-(?:last-)?of-type$/.test(nthName)) {
238
+ const anb = Object.fromEntries(anbMap);
239
+ const arr = collectNthOfType(anb, node);
240
+ if (arr.length) {
241
+ matched.push(...arr);
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ return [...new Set(matched)];
133
249
  };
134
250
 
135
251
  /**
@@ -196,7 +312,7 @@ const matchClassSelector = (ast = {}, node = {}) => {
196
312
  * @param {object} node - Element node
197
313
  * @returns {?object} - matched node
198
314
  */
199
- const matchIdSelector = (ast = {}, node = {}) => {
315
+ const matchIDSelector = (ast = {}, node = {}) => {
200
316
  const { name: astName, type: astType } = ast;
201
317
  const { id, nodeType } = node;
202
318
  let res;
@@ -337,41 +453,35 @@ const matchAttributeSelector = (ast = {}, node = {}) => {
337
453
  break;
338
454
  case '|=':
339
455
  if (attrValue) {
340
- for (const item of attrValues) {
341
- if (item === attrValue || item.startsWith(`${attrValue}-`)) {
342
- res = node;
343
- break;
344
- }
456
+ const item = attrValues.find(v =>
457
+ (v === attrValue || v.startsWith(`${attrValue}-`))
458
+ );
459
+ if (item) {
460
+ res = node;
345
461
  }
346
462
  }
347
463
  break;
348
464
  case '^=':
349
465
  if (attrValue) {
350
- for (const item of attrValues) {
351
- if (item.startsWith(`${attrValue}`)) {
352
- res = node;
353
- break;
354
- }
466
+ const item = attrValues.find(v => v.startsWith(`${attrValue}`));
467
+ if (item) {
468
+ res = node;
355
469
  }
356
470
  }
357
471
  break;
358
472
  case '$=':
359
473
  if (attrValue) {
360
- for (const item of attrValues) {
361
- if (item.endsWith(`${attrValue}`)) {
362
- res = node;
363
- break;
364
- }
474
+ const item = attrValues.find(v => v.endsWith(`${attrValue}`));
475
+ if (item) {
476
+ res = node;
365
477
  }
366
478
  }
367
479
  break;
368
480
  case '*=':
369
481
  if (attrValue) {
370
- for (const item of attrValues) {
371
- if (item.includes(`${attrValue}`)) {
372
- res = node;
373
- break;
374
- }
482
+ const item = attrValues.find(v => v.includes(`${attrValue}`));
483
+ if (item) {
484
+ res = node;
375
485
  }
376
486
  }
377
487
  break;
@@ -384,89 +494,7 @@ const matchAttributeSelector = (ast = {}, node = {}) => {
384
494
  };
385
495
 
386
496
  /**
387
- * match An+B
388
- * @param {string} nthName - nth pseudo class name
389
- * @param {object} ast - AST
390
- * @param {object} node - Element node
391
- * @returns {Array.<object|undefined>} - collection of matched nodes
392
- */
393
- const matchAnPlusB = (nthName, ast = {}, node = {}) => {
394
- const res = new Set();
395
- if (typeof nthName === 'string') {
396
- nthName = nthName.trim();
397
- if (REG_PSEUDO_NTH.test(nthName)) {
398
- const {
399
- nth: {
400
- a,
401
- b,
402
- name: identName
403
- },
404
- selector: astSelector,
405
- type: astType
406
- } = ast;
407
- const { nodeType } = node;
408
- if (astType === NTH && nodeType === ELEMENT_NODE) {
409
- /*
410
- // FIXME:
411
- // :nth-child(An+B of S)
412
- if (astSelector) {
413
- }
414
- */
415
- if (!astSelector) {
416
- const optMap = new Map();
417
- if (identName) {
418
- if (identName === 'even') {
419
- optMap.set('a', 2);
420
- optMap.set('b', 0);
421
- } else if (identName === 'odd') {
422
- optMap.set('a', 2);
423
- optMap.set('b', 1);
424
- }
425
- if (/last/.test(nthName)) {
426
- optMap.set('reverse', true);
427
- }
428
- } else {
429
- if (typeof a === 'string' && /-?\d+/.test(a)) {
430
- optMap.set('a', a * 1);
431
- } else {
432
- optMap.set('a', 0);
433
- }
434
- if (typeof b === 'string' && /-?\d+/.test(b)) {
435
- optMap.set('b', b * 1);
436
- } else {
437
- optMap.set('b', 0);
438
- }
439
- if (/last/.test(nthName)) {
440
- optMap.set('reverse', true);
441
- }
442
- }
443
- if (optMap.size > 1) {
444
- const opt = Object.fromEntries(optMap);
445
- if (/^nth-(?:last-)?child$/.test(nthName)) {
446
- const arr = collectNthChild(node, opt);
447
- if (arr.length) {
448
- for (const i of arr) {
449
- res.add(i);
450
- }
451
- }
452
- } else if (/^nth-(?:last-)?of-type$/.test(nthName)) {
453
- const arr = collectNthOfType(node, opt);
454
- if (arr.length) {
455
- for (const i of arr) {
456
- res.add(i);
457
- }
458
- }
459
- }
460
- }
461
- }
462
- }
463
- }
464
- }
465
- return [...res];
466
- };
467
-
468
- /**
469
- * match language pseudo class
497
+ * match language pseudo-class
470
498
  * @see https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1
471
499
  * @param {object} ast - AST
472
500
  * @param {object} node - Element node
@@ -477,16 +505,16 @@ const matchLanguagePseudoClass = (ast = {}, node = {}) => {
477
505
  const { lang, nodeType } = node;
478
506
  let res;
479
507
  if (astType === IDENTIFIER && nodeType === ELEMENT_NODE) {
480
- // FIXME:
481
- /*
508
+ // TBD: what about deprecated xml:lang?
482
509
  if (astName === '') {
483
- if (!lang) {
510
+ if (node.getAttribute('lang') === '') {
484
511
  res = node;
485
512
  }
486
513
  } else if (astName === '*') {
487
- }
488
- */
489
- if (/[A-Za-z\d-]+/.test(astName)) {
514
+ if (!node.hasAttribute('lang')) {
515
+ res = node;
516
+ }
517
+ } else if (/[A-Za-z\d-]+/.test(astName)) {
490
518
  const codePart = '(?:-[A-Za-z\\d]+)?';
491
519
  let reg;
492
520
  if (/-/.test(astName)) {
@@ -523,7 +551,8 @@ const matchLanguagePseudoClass = (ast = {}, node = {}) => {
523
551
  };
524
552
 
525
553
  /**
526
- * match pseudo class selector
554
+ * match pseudo-class selector
555
+ * @see https://html.spec.whatwg.org/#pseudo-classes
527
556
  * @param {object} ast - AST
528
557
  * @param {object} node - Element node
529
558
  * @param {object} [refPoint] - reference point
@@ -535,38 +564,36 @@ const matchPseudoClassSelector = (
535
564
  refPoint = {}
536
565
  ) => {
537
566
  const { children: astChildren, name: astName, type: astType } = ast;
538
- const { nodeType, ownerDocument } = node;
539
- const res = new Set();
567
+ const { localName, nodeType, ownerDocument, parentNode } = node;
568
+ const matched = [];
540
569
  if (astType === PSEUDO_CLASS_SELECTOR && nodeType === ELEMENT_NODE) {
541
570
  if (Array.isArray(astChildren)) {
542
571
  const [astChildAst] = astChildren;
543
572
  // :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
544
- if (REG_PSEUDO_NTH.test(astName)) {
573
+ if (PSEUDO_NTH.test(astName)) {
545
574
  const arr = matchAnPlusB(astName, astChildAst, node);
546
575
  if (arr.length) {
547
- for (const i of arr) {
548
- res.add(i);
549
- }
576
+ matched.push(...arr);
550
577
  }
551
578
  } else {
552
579
  switch (astName) {
553
580
  case 'dir':
554
581
  if (astChildAst.name === node.dir) {
555
- res.add(node);
582
+ matched.push(node);
556
583
  }
557
584
  break;
558
585
  case 'lang':
559
586
  if (matchLanguagePseudoClass(astChildAst, node)) {
560
- res.add(node);
587
+ matched.push(node);
561
588
  }
562
589
  break;
563
590
  case 'current':
564
591
  case 'nth-col':
565
592
  case 'nth-last-col':
566
- console.warn(`Unsupported pseudo class ${astName}`);
593
+ console.warn(`Unsupported pseudo-class ${astName}`);
567
594
  break;
568
595
  default:
569
- console.warn(`Unknown pseudo class ${astName}`);
596
+ console.warn(`Unknown pseudo-class ${astName}`);
570
597
  }
571
598
  }
572
599
  } else {
@@ -575,18 +602,18 @@ const matchPseudoClassSelector = (
575
602
  switch (astName) {
576
603
  case 'any-link':
577
604
  case 'link':
578
- // FIXME: what about namespaced href? e.g. xlink:href
605
+ // TBD: what about namespaced href? e.g. xlink:href
579
606
  if (node.hasAttribute('href')) {
580
- res.add(node);
607
+ matched.push(node);
581
608
  }
582
609
  break;
583
610
  case 'local-link':
584
- // FIXME: what about namespaced href? e.g. xlink:href
611
+ // TBD: what about namespaced href? e.g. xlink:href
585
612
  if (node.hasAttribute('href')) {
586
613
  const attrURL = new URL(node.getAttribute('href'), docURL.href);
587
614
  if (attrURL.origin === docURL.origin &&
588
615
  attrURL.pathname === docURL.pathname) {
589
- res.add(node);
616
+ matched.push(node);
590
617
  }
591
618
  }
592
619
  break;
@@ -595,7 +622,7 @@ const matchPseudoClassSelector = (
595
622
  break;
596
623
  case 'target':
597
624
  if (docURL.hash && node.id && docURL.hash === `#${node.id}`) {
598
- res.add(node);
625
+ matched.push(node);
599
626
  }
600
627
  break;
601
628
  case 'target-within':
@@ -604,7 +631,7 @@ const matchPseudoClassSelector = (
604
631
  let current = ownerDocument.getElementById(hash);
605
632
  while (current) {
606
633
  if (current === node) {
607
- res.add(node);
634
+ matched.push(node);
608
635
  break;
609
636
  }
610
637
  current = current.parentNode;
@@ -614,22 +641,22 @@ const matchPseudoClassSelector = (
614
641
  case 'scope':
615
642
  if (refPoint?.nodeType === ELEMENT_NODE) {
616
643
  if (node === refPoint) {
617
- res.add(node);
644
+ matched.push(node);
618
645
  }
619
646
  } else if (node === root) {
620
- res.add(node);
647
+ matched.push(node);
621
648
  }
622
649
  break;
623
650
  case 'focus':
624
651
  if (node === ownerDocument.activeElement) {
625
- res.add(node);
652
+ matched.push(node);
626
653
  }
627
654
  break;
628
655
  case 'focus-within': {
629
656
  let current = ownerDocument.activeElement;
630
657
  while (current) {
631
658
  if (current === node) {
632
- res.add(node);
659
+ matched.push(node);
633
660
  break;
634
661
  }
635
662
  current = current.parentNode;
@@ -637,97 +664,154 @@ const matchPseudoClassSelector = (
637
664
  break;
638
665
  }
639
666
  case 'open':
640
- if (node.hasAttribute('open')) {
641
- res.add(node);
667
+ if (HTML_INTERACT.test(localName) && node.hasAttribute('open')) {
668
+ matched.push(node);
642
669
  }
643
670
  break;
644
671
  case 'closed':
645
- // FIXME: is this really okay?
646
- if (!node.hasAttribute('open')) {
647
- res.add(node);
672
+ if (HTML_INTERACT.test(localName) && !node.hasAttribute('open')) {
673
+ matched.push(node);
648
674
  }
649
675
  break;
650
676
  case 'disabled':
651
- if (node.hasAttribute('disabled')) {
652
- res.add(node);
677
+ if ((HTML_FORM_INPUT.test(localName) ||
678
+ HTML_FORM_PARTS.test(localName) ||
679
+ HTML_CUSTOM_ELEMENT.test(localName)) &&
680
+ node.hasAttribute('disabled')) {
681
+ matched.push(node);
653
682
  }
654
683
  break;
655
684
  case 'enabled':
656
- // FIXME: is this really okay?
657
- if (!node.hasAttribute('disabled')) {
658
- res.add(node);
685
+ if ((HTML_FORM_INPUT.test(localName) ||
686
+ HTML_FORM_PARTS.test(localName) ||
687
+ HTML_CUSTOM_ELEMENT.test(localName)) &&
688
+ !node.hasAttribute('disabled')) {
689
+ matched.push(node);
659
690
  }
660
691
  break;
661
692
  case 'checked':
662
- if (node.checked) {
663
- res.add(node);
693
+ if ((/^input$/.test(localName) && node.hasAttribute('type') &&
694
+ /^(?:checkbox|radio)$/.test(node.getAttribute('type')) &&
695
+ node.checked) ||
696
+ (localName === 'option' && node.selected)) {
697
+ matched.push(node);
698
+ }
699
+ break;
700
+ case 'default':
701
+ // input[type="checkbox"], input[type="radio"]
702
+ if (/^input$/.test(localName) && node.hasAttribute('type') &&
703
+ /^(?:checkbox|radio)$/.test(node.getAttribute('type'))) {
704
+ if (node.hasAttribute('checked')) {
705
+ matched.push(node);
706
+ }
707
+ // option
708
+ } else if (localName === 'option') {
709
+ let isMultiple = false;
710
+ let parent = parentNode;
711
+ while (parent) {
712
+ if (parent.localName === 'datalist') {
713
+ break;
714
+ } else if (parent.localName === 'select') {
715
+ isMultiple = !!parent.multiple;
716
+ break;
717
+ }
718
+ parent = parent.parentNode;
719
+ }
720
+ // FIXME:
721
+ if (isMultiple) {
722
+ console.warn(`Unsupported pseudo-class ${astName}`);
723
+ } else {
724
+ const firstOpt = parentNode.firstElementChild;
725
+ const defaultOpt = [];
726
+ let opt = firstOpt;
727
+ while (opt) {
728
+ if (opt.hasAttribute('selected')) {
729
+ defaultOpt.push(opt);
730
+ break;
731
+ }
732
+ opt = opt.nextElementSibling;
733
+ }
734
+ if (!defaultOpt.length) {
735
+ defaultOpt.push(firstOpt);
736
+ }
737
+ if (defaultOpt.includes(node)) {
738
+ matched.push(node);
739
+ }
740
+ }
741
+ // FIXME:
742
+ // button[type="submit"], input[type="submit"], input[type="image"]
743
+ } else if ((localName === 'button' &&
744
+ (!node.hasAttribute('type') ||
745
+ node.getAttribute('type') === 'submit')) ||
746
+ (/^input$/.test(localName) && node.hasAttribute('type') &&
747
+ /^(?:image|submit)$/.test(node.getAttribute('type')))) {
748
+ console.warn(`Unsupported pseudo-class ${astName}`);
664
749
  }
665
750
  break;
666
751
  case 'required':
667
- if (node.required) {
668
- res.add(node);
752
+ if (HTML_FORM_INPUT.test(localName) && node.required) {
753
+ matched.push(node);
669
754
  }
670
755
  break;
671
756
  case 'optional':
672
- // FIXME: is this really okay?
673
- if (!node.required) {
674
- res.add(node);
757
+ if (HTML_FORM_INPUT.test(localName) && !node.required) {
758
+ matched.push(node);
675
759
  }
676
760
  break;
677
761
  case 'root':
678
762
  if (node === root) {
679
- res.add(node);
763
+ matched.push(node);
680
764
  }
681
765
  break;
682
766
  case 'first-child':
683
767
  if (node === node.parentNode.firstElementChild) {
684
- res.add(node);
768
+ matched.push(node);
685
769
  }
686
770
  break;
687
771
  case 'last-child':
688
772
  if (node === node.parentNode.lastElementChild) {
689
- res.add(node);
773
+ matched.push(node);
690
774
  }
691
775
  break;
692
776
  case 'only-child':
693
777
  if (node === node.parentNode.firstElementChild &&
694
778
  node === node.parentNode.lastElementChild) {
695
- res.add(node);
779
+ matched.push(node);
696
780
  }
697
781
  break;
698
782
  case 'first-of-type': {
699
- const [node1] = collectNthOfType(node, {
783
+ const [node1] = collectNthOfType({
700
784
  a: 0,
701
785
  b: 0
702
- });
786
+ }, node);
703
787
  if (node1) {
704
- res.add(node1);
788
+ matched.push(node1);
705
789
  }
706
790
  break;
707
791
  }
708
792
  case 'last-of-type': {
709
- const [node1] = collectNthOfType(node, {
793
+ const [node1] = collectNthOfType({
710
794
  a: 0,
711
795
  b: 0,
712
796
  reverse: true
713
- });
797
+ }, node);
714
798
  if (node1) {
715
- res.add(node1);
799
+ matched.push(node1);
716
800
  }
717
801
  break;
718
802
  }
719
803
  case 'only-of-type': {
720
- const [node1] = collectNthOfType(node, {
804
+ const [node1] = collectNthOfType({
721
805
  a: 0,
722
806
  b: 0
723
- });
724
- const [node2] = collectNthOfType(node, {
807
+ }, node);
808
+ const [node2] = collectNthOfType({
725
809
  a: 0,
726
810
  b: 0,
727
811
  reverse: true
728
- });
812
+ }, node);
729
813
  if (node1 === node && node2 === node) {
730
- res.add(node);
814
+ matched.push(node);
731
815
  }
732
816
  break;
733
817
  }
@@ -736,7 +820,6 @@ const matchPseudoClassSelector = (
736
820
  case 'blank':
737
821
  case 'buffering':
738
822
  case 'current':
739
- case 'default':
740
823
  case 'empty':
741
824
  case 'focus-visible':
742
825
  case 'fullscreen':
@@ -761,14 +844,14 @@ const matchPseudoClassSelector = (
761
844
  case 'user-valid':
762
845
  case 'valid':
763
846
  case 'volume-locked':
764
- console.warn(`Unsupported pseudo class ${astName}`);
847
+ console.warn(`Unsupported pseudo-class ${astName}`);
765
848
  break;
766
849
  default:
767
- console.warn(`Unknown pseudo class ${astName}`);
850
+ console.warn(`Unknown pseudo-class ${astName}`);
768
851
  }
769
852
  }
770
853
  }
771
- return [...res];
854
+ return [...new Set(matched)];
772
855
  };
773
856
 
774
857
  /**
@@ -817,20 +900,18 @@ class Matcher {
817
900
  * @param {object} node - Element node
818
901
  * @returns {Array.<object|undefined>} - collection of matched nodes
819
902
  */
820
- _parseAst(ast, node) {
821
- const items = walkAst(ast);
822
- const res = new Set();
903
+ _parseAST(ast, node) {
904
+ const items = walkAST(ast);
905
+ const matched = [];
823
906
  if (items.length) {
824
907
  for (const item of items) {
825
908
  const arr = this._matchSelector(item, node);
826
909
  if (arr.length) {
827
- for (const i of arr) {
828
- res.add(i);
829
- }
910
+ matched.push(...arr);
830
911
  }
831
912
  }
832
913
  }
833
- return [...res];
914
+ return [...new Set(matched)];
834
915
  }
835
916
 
836
917
  /**
@@ -843,28 +924,22 @@ class Matcher {
843
924
  const [prevLeaf, nextLeaf] = leaves;
844
925
  const iterator = this._createIterator(prevLeaf, node);
845
926
  let prevNode = iterator.nextNode();
846
- const nodes = new Set();
927
+ const items = [];
847
928
  while (prevNode) {
848
929
  const arr = this._match(prevLeaf, prevNode);
849
930
  if (arr.length) {
850
931
  for (const item of arr) {
851
932
  const a = this._match(nextLeaf, item);
852
933
  if (a.length) {
853
- for (const i of a) {
854
- nodes.add(i);
855
- }
934
+ items.push(...a);
856
935
  }
857
936
  }
858
937
  }
859
938
  prevNode = iterator.nextNode();
860
939
  }
861
- const items = [...nodes];
862
940
  let res;
863
- for (const item of items) {
864
- if (item === node) {
865
- res = item;
866
- break;
867
- }
941
+ if (items.includes(node)) {
942
+ res = node;
868
943
  }
869
944
  return res || null;
870
945
  }
@@ -890,9 +965,7 @@ class Matcher {
890
965
  while (nextNode) {
891
966
  const arr = this._match(item, nextNode);
892
967
  if (arr.length) {
893
- for (const i of arr) {
894
- nodes.add(i);
895
- }
968
+ nodes.add(...arr);
896
969
  }
897
970
  nextNode = iterator.nextNode();
898
971
  }
@@ -908,22 +981,22 @@ class Matcher {
908
981
  }
909
982
  }
910
983
  }
911
- const res = new Set();
984
+ const matched = [];
912
985
  if (nodes.size && /^[ >+~]$/.test(comboName)) {
913
- const items = [...nodes];
914
- for (const item of items) {
986
+ const arr = [...nodes];
987
+ for (const item of arr) {
915
988
  let refNode = item;
916
989
  switch (comboName) {
917
990
  case '>':
918
991
  if (refNode.parentNode === prevNode) {
919
- res.add(item);
992
+ matched.push(item);
920
993
  }
921
994
  break;
922
995
  case '~':
923
996
  refNode = refNode.previousElementSibling;
924
997
  while (refNode) {
925
998
  if (refNode === prevNode) {
926
- res.add(item);
999
+ matched.push(item);
927
1000
  break;
928
1001
  }
929
1002
  refNode = refNode.previousElementSibling;
@@ -931,14 +1004,14 @@ class Matcher {
931
1004
  break;
932
1005
  case '+':
933
1006
  if (refNode.previousElementSibling === prevNode) {
934
- res.add(item);
1007
+ matched.push(item);
935
1008
  }
936
1009
  break;
937
1010
  default:
938
1011
  refNode = refNode.parentNode;
939
1012
  while (refNode) {
940
1013
  if (refNode === prevNode) {
941
- res.add(item);
1014
+ matched.push(item);
942
1015
  break;
943
1016
  }
944
1017
  refNode = refNode.parentNode;
@@ -946,7 +1019,7 @@ class Matcher {
946
1019
  }
947
1020
  }
948
1021
  }
949
- return [...res];
1022
+ return [...new Set(matched)];
950
1023
  }
951
1024
 
952
1025
  /**
@@ -958,27 +1031,25 @@ class Matcher {
958
1031
  _matchArgumentLeaf(leaf, node) {
959
1032
  const iterator = this._createIterator(leaf, node);
960
1033
  let nextNode = iterator.nextNode();
961
- const res = new Set();
1034
+ const matched = [];
962
1035
  while (nextNode) {
963
1036
  const arr = this._match(leaf, nextNode);
964
1037
  if (arr.length) {
965
- for (const i of arr) {
966
- res.add(i);
967
- }
1038
+ matched.push(...arr);
968
1039
  }
969
1040
  nextNode = iterator.nextNode();
970
1041
  }
971
- return [...res];
1042
+ return [...new Set(matched)];
972
1043
  }
973
1044
 
974
1045
  /**
975
- * match logical pseudo class functions - :is(), :has(), :not(), :where()
1046
+ * match logical pseudo-class functions - :is(), :has(), :not(), :where()
976
1047
  * @param {object} branch - AST branch
977
1048
  * @param {object} node - Element node
978
1049
  * @returns {?object} - matched node
979
1050
  */
980
1051
  _matchLogicalPseudoFunc(branch, node) {
981
- const ast = walkAst(branch);
1052
+ const ast = walkAST(branch);
982
1053
  let res;
983
1054
  if (ast.length) {
984
1055
  const { name: branchName } = branch;
@@ -1078,13 +1149,13 @@ class Matcher {
1078
1149
  * @returns {Array.<object|undefined>} - collection of matched nodes
1079
1150
  */
1080
1151
  _matchSelector(children, node) {
1081
- const res = new Set();
1152
+ const matched = [];
1082
1153
  if (Array.isArray(children) && children.length) {
1083
1154
  const [firstChild] = children;
1084
1155
  let iteratorLeaf;
1085
1156
  if (firstChild.type === COMBINATOR ||
1086
1157
  (firstChild.type === PSEUDO_CLASS_SELECTOR &&
1087
- REG_PSEUDO_NTH.test(firstChild.name))) {
1158
+ PSEUDO_NTH.test(firstChild.name))) {
1088
1159
  iteratorLeaf = {
1089
1160
  name: '*',
1090
1161
  type: TYPE_SELECTOR
@@ -1102,18 +1173,16 @@ class Matcher {
1102
1173
  const item = items.shift();
1103
1174
  const { name: itemName, type: itemType } = item;
1104
1175
  if (itemType === PSEUDO_CLASS_SELECTOR &&
1105
- REG_PSEUDO_FUNC.test(itemName)) {
1176
+ PSEUDO_FUNC.test(itemName)) {
1106
1177
  nextNode = this._matchLogicalPseudoFunc(item, nextNode);
1107
1178
  if (nextNode) {
1108
- res.add(nextNode);
1179
+ matched.push(nextNode);
1109
1180
  nextNode = null;
1110
1181
  }
1111
1182
  } else {
1112
1183
  const arr = this._match(item, nextNode);
1113
1184
  if (arr.length) {
1114
- for (const i of arr) {
1115
- res.add(i);
1116
- }
1185
+ matched.push(...arr);
1117
1186
  }
1118
1187
  }
1119
1188
  } else {
@@ -1121,7 +1190,7 @@ class Matcher {
1121
1190
  const item = items.shift();
1122
1191
  const { name: itemName, type: itemType } = item;
1123
1192
  if (itemType === PSEUDO_CLASS_SELECTOR &&
1124
- REG_PSEUDO_FUNC.test(itemName)) {
1193
+ PSEUDO_FUNC.test(itemName)) {
1125
1194
  nextNode = this._matchLogicalPseudoFunc(item, nextNode);
1126
1195
  } else if (itemType === COMBINATOR) {
1127
1196
  const leaves = [];
@@ -1130,9 +1199,9 @@ class Matcher {
1130
1199
  const [nextItem] = items;
1131
1200
  if (nextItem.type === COMBINATOR ||
1132
1201
  (nextItem.type === PSEUDO_CLASS_SELECTOR &&
1133
- REG_PSEUDO_NTH.test(nextItem.name)) ||
1202
+ PSEUDO_NTH.test(nextItem.name)) ||
1134
1203
  (nextItem.type === PSEUDO_CLASS_SELECTOR &&
1135
- REG_PSEUDO_FUNC.test(nextItem.name))) {
1204
+ PSEUDO_FUNC.test(nextItem.name))) {
1136
1205
  break;
1137
1206
  } else {
1138
1207
  leaves.push(items.shift());
@@ -1146,15 +1215,11 @@ class Matcher {
1146
1215
  for (const i of arr) {
1147
1216
  const a = this._matchSelector(items, i);
1148
1217
  if (a.length) {
1149
- for (const j of a) {
1150
- res.add(j);
1151
- }
1218
+ matched.push(...a);
1152
1219
  }
1153
1220
  }
1154
1221
  } else {
1155
- for (const i of arr) {
1156
- res.add(i);
1157
- }
1222
+ matched.push(...arr);
1158
1223
  }
1159
1224
  nextNode = null;
1160
1225
  }
@@ -1163,28 +1228,28 @@ class Matcher {
1163
1228
  }
1164
1229
  } while (items.length && nextNode);
1165
1230
  if (nextNode) {
1166
- res.add(nextNode);
1231
+ matched.push(nextNode);
1167
1232
  }
1168
1233
  }
1169
1234
  } else if (nextNode) {
1170
- res.add(nextNode);
1235
+ matched.push(nextNode);
1171
1236
  }
1172
1237
  nextNode = iterator.nextNode();
1173
1238
  }
1174
1239
  } else if (firstChild.type === PSEUDO_CLASS_SELECTOR &&
1175
- REG_PSEUDO_FUNC.test(firstChild.name) &&
1240
+ PSEUDO_FUNC.test(firstChild.name) &&
1176
1241
  node.nodeType === ELEMENT_NODE) {
1177
1242
  nextNode = node;
1178
1243
  while (nextNode) {
1179
1244
  nextNode = this._matchLogicalPseudoFunc(firstChild, nextNode);
1180
1245
  if (nextNode) {
1181
- res.add(nextNode);
1246
+ matched.push(nextNode);
1182
1247
  }
1183
1248
  nextNode = nextNode.nextElementSibling;
1184
1249
  }
1185
1250
  }
1186
1251
  }
1187
- return [...res];
1252
+ return [...new Set(matched)];
1188
1253
  }
1189
1254
 
1190
1255
  /**
@@ -1194,49 +1259,45 @@ class Matcher {
1194
1259
  * @returns {Array.<object|undefined>} - collection of matched nodes
1195
1260
  */
1196
1261
  _match(ast = this.#ast, node = this.#node) {
1197
- const res = new Set();
1262
+ const matched = [];
1198
1263
  const { name, type } = ast;
1199
1264
  switch (type) {
1200
1265
  case TYPE_SELECTOR:
1201
1266
  if (matchTypeSelector(ast, node)) {
1202
- res.add(node);
1267
+ matched.push(node);
1203
1268
  }
1204
1269
  break;
1205
1270
  case CLASS_SELECTOR:
1206
1271
  if (matchClassSelector(ast, node)) {
1207
- res.add(node);
1272
+ matched.push(node);
1208
1273
  }
1209
1274
  break;
1210
1275
  case ID_SELECTOR:
1211
- if (matchIdSelector(ast, node)) {
1212
- res.add(node);
1276
+ if (matchIDSelector(ast, node)) {
1277
+ matched.push(node);
1213
1278
  }
1214
1279
  break;
1215
1280
  case ATTRIBUTE_SELECTOR:
1216
1281
  if (matchAttributeSelector(ast, node)) {
1217
- res.add(node);
1282
+ matched.push(node);
1218
1283
  }
1219
1284
  break;
1220
1285
  case PSEUDO_CLASS_SELECTOR:
1221
- if (!REG_PSEUDO_FUNC.test(name)) {
1286
+ if (!PSEUDO_FUNC.test(name)) {
1222
1287
  const arr = matchPseudoClassSelector(ast, node, this.#node);
1223
1288
  if (arr.length) {
1224
- for (const i of arr) {
1225
- res.add(i);
1226
- }
1289
+ matched.push(...arr);
1227
1290
  }
1228
1291
  }
1229
1292
  break;
1230
1293
  default: {
1231
- const arr = this._parseAst(ast, node);
1294
+ const arr = this._parseAST(ast, node);
1232
1295
  if (arr.length) {
1233
- for (const i of arr) {
1234
- res.add(i);
1235
- }
1296
+ matched.push(...arr);
1236
1297
  }
1237
1298
  }
1238
1299
  }
1239
- return [...res];
1300
+ return [...new Set(matched)];
1240
1301
  }
1241
1302
 
1242
1303
  /**
@@ -1245,16 +1306,7 @@ class Matcher {
1245
1306
  */
1246
1307
  matches() {
1247
1308
  const arr = this._match(this.#ast, this.#document);
1248
- const node = this.#node;
1249
- let res;
1250
- if (arr.length) {
1251
- for (const i of arr) {
1252
- if (i === node) {
1253
- res = true;
1254
- break;
1255
- }
1256
- }
1257
- }
1309
+ const res = arr.length && arr.includes(this.#node);
1258
1310
  return !!res;
1259
1311
  }
1260
1312
 
@@ -1267,13 +1319,8 @@ class Matcher {
1267
1319
  let node = this.#node;
1268
1320
  let res;
1269
1321
  while (node) {
1270
- for (const i of arr) {
1271
- if (i === node) {
1272
- res = i;
1273
- break;
1274
- }
1275
- }
1276
- if (res) {
1322
+ if (arr.includes(node)) {
1323
+ res = node;
1277
1324
  break;
1278
1325
  }
1279
1326
  node = node.parentNode;
@@ -1306,15 +1353,13 @@ class Matcher {
1306
1353
  */
1307
1354
  querySelectorAll() {
1308
1355
  const arr = this._match(this.#ast, this.#node);
1309
- const res = new Set();
1310
1356
  if (arr.length) {
1311
- for (const i of arr) {
1312
- if (i !== this.#node) {
1313
- res.add(i);
1314
- }
1357
+ const i = arr.findIndex(node => node === this.#node);
1358
+ if (i >= 0) {
1359
+ arr.splice(i, 1);
1315
1360
  }
1316
1361
  }
1317
- return [...res];
1362
+ return [...new Set(arr)];
1318
1363
  }
1319
1364
  };
1320
1365
 
@@ -1325,7 +1370,7 @@ module.exports = {
1325
1370
  matchAnPlusB,
1326
1371
  matchAttributeSelector,
1327
1372
  matchClassSelector,
1328
- matchIdSelector,
1373
+ matchIDSelector,
1329
1374
  matchLanguagePseudoClass,
1330
1375
  matchPseudoClassSelector,
1331
1376
  matchTypeSelector
package/src/js/parser.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  /* api */
6
- const { parse, toPlainObject, walk } = require('css-tree');
6
+ const { generate, parse, toPlainObject, walk } = require('css-tree');
7
7
  const { SELECTOR } = require('./constant.js');
8
8
 
9
9
  /**
@@ -24,7 +24,7 @@ const parseSelector = selector => {
24
24
  * @param {object} ast - AST
25
25
  * @returns {Array.<object|undefined>} - collection of AST branches
26
26
  */
27
- const walkAst = (ast = {}) => {
27
+ const walkAST = (ast = {}) => {
28
28
  const selectors = new Set();
29
29
  const opt = {
30
30
  enter: branch => {
@@ -35,7 +35,7 @@ const walkAst = (ast = {}) => {
35
35
  leave: branch => {
36
36
  let skip;
37
37
  if (branch.type === SELECTOR) {
38
- skip = walkAst.skip;
38
+ skip = walkAST.skip;
39
39
  }
40
40
  return skip;
41
41
  }
@@ -46,6 +46,7 @@ const walkAst = (ast = {}) => {
46
46
 
47
47
  /* export */
48
48
  module.exports = {
49
+ generateCSS: generate,
49
50
  parseSelector,
50
- walkAst
51
+ walkAST
51
52
  };
@@ -1,7 +1,7 @@
1
1
  export class Matcher {
2
2
  constructor(selector: string, refPoint: object);
3
3
  _createIterator(ast?: object, root?: object): object;
4
- _parseAst(ast: object, node: object): Array<object | undefined>;
4
+ _parseAST(ast: object, node: object): Array<object | undefined>;
5
5
  _matchAdjacentLeaves(leaves: Array<object>, node: object): object | null;
6
6
  _matchCombinator(leaves: Array<object>, prevNode: object): Array<object | undefined>;
7
7
  _matchArgumentLeaf(leaf: object, node: object): Array<object | undefined>;
@@ -14,20 +14,21 @@ export class Matcher {
14
14
  querySelectorAll(): Array<object | undefined>;
15
15
  #private;
16
16
  }
17
- export function collectNthChild(node?: object, opt?: {
17
+ export function collectNthChild(anb?: {
18
18
  a: number;
19
19
  b: number;
20
20
  reverse?: boolean;
21
- }): Array<object | undefined>;
22
- export function collectNthOfType(node?: object, opt?: {
21
+ selector?: string;
22
+ }, node?: object): Array<object | undefined>;
23
+ export function collectNthOfType(anb?: {
23
24
  a: number;
24
25
  b: number;
25
26
  reverse?: boolean;
26
- }): Array<object | undefined>;
27
+ }, node?: object): Array<object | undefined>;
27
28
  export function matchAnPlusB(nthName: string, ast?: object, node?: object): Array<object | undefined>;
28
29
  export function matchAttributeSelector(ast?: object, node?: object): object | null;
29
30
  export function matchClassSelector(ast?: object, node?: object): object | null;
30
- export function matchIdSelector(ast?: object, node?: object): object | null;
31
+ export function matchIDSelector(ast?: object, node?: object): object | null;
31
32
  export function matchLanguagePseudoClass(ast?: object, node?: object): object | null;
32
33
  export function matchPseudoClassSelector(ast?: object, node?: object, refPoint?: object): Array<object | undefined>;
33
34
  export function matchTypeSelector(ast?: object, node?: object): object | null;
@@ -1,2 +1,4 @@
1
+ import { generate } from "css-tree";
1
2
  export function parseSelector(selector: string): object;
2
- export function walkAst(ast?: object): Array<object | undefined>;
3
+ export function walkAST(ast?: object): Array<object | undefined>;
4
+ export { generate as generateCSS };