@asamuzakjp/dom-selector 0.2.1

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.
@@ -0,0 +1,1257 @@
1
+ /**
2
+ * matcher.js
3
+ */
4
+
5
+ /* import */
6
+ const { parseSelector, walkAst } = require('./parser.js');
7
+
8
+ /* constants */
9
+ const {
10
+ ATTRIBUTE_SELECTOR, CLASS_SELECTOR, COMBINATOR, IDENTIFIER, ID_SELECTOR,
11
+ NTH, PSEUDO_CLASS_SELECTOR, TYPE_SELECTOR
12
+ } = require('./constant.js');
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)$/;
16
+
17
+ /**
18
+ * collect nth child
19
+ * @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
+ * @returns {Array.<object|undefined>} - collection of matched nodes
25
+ */
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)) {
32
+ const arr = [...parentNode.children];
33
+ if (reverse) {
34
+ arr.reverse();
35
+ }
36
+ const l = arr.length;
37
+ // :first-child, :last-child
38
+ if (a === 0) {
39
+ if (b >= 0 && b < l) {
40
+ const item = arr[b];
41
+ res.add(item);
42
+ }
43
+ // :nth-child()
44
+ } else {
45
+ let n = 0;
46
+ let nth = b - 1;
47
+ while (nth < 0) {
48
+ nth += (++n * a);
49
+ }
50
+ let i = 0;
51
+ while (i < l && nth < l) {
52
+ if (i === nth) {
53
+ const item = arr[i];
54
+ res.add(item);
55
+ nth += a;
56
+ }
57
+ i++;
58
+ }
59
+ }
60
+ }
61
+ return [...res];
62
+ };
63
+
64
+ /**
65
+ * collect nth of type
66
+ * @param {object} node - element node
67
+ * @param {object} opt - options
68
+ * @param {number} opt.a - a
69
+ * @param {number} opt.b - b
70
+ * @param {boolean} [opt.reverse] - reverse order
71
+ * @returns {Array.<object|undefined>} - collection of matched nodes
72
+ */
73
+ const collectNthOfType = (node = {}, opt = {}) => {
74
+ const { localName, nodeType, parentNode, prefix } = node;
75
+ const { a, b, reverse } = opt;
76
+ const res = new Set();
77
+ if (nodeType === ELEMENT_NODE &&
78
+ Number.isInteger(a) && Number.isInteger(b)) {
79
+ const arr = [...parentNode.children];
80
+ if (reverse) {
81
+ arr.reverse();
82
+ }
83
+ const l = arr.length;
84
+ // :first-of-type, :last-of-type
85
+ if (a === 0) {
86
+ if (b >= 0 && b < l) {
87
+ let i = 0;
88
+ let j = 0;
89
+ while (i < l) {
90
+ const item = arr[i];
91
+ const { localName: itemLocalName, prefix: itemPrefix } = item;
92
+ if (itemLocalName === localName && itemPrefix === prefix) {
93
+ if (j === b) {
94
+ res.add(item);
95
+ break;
96
+ }
97
+ j++;
98
+ }
99
+ i++;
100
+ }
101
+ }
102
+ // :nth-of-type()
103
+ } else {
104
+ let nth = b - 1;
105
+ while (nth < 0) {
106
+ nth += a;
107
+ }
108
+ let i = 0;
109
+ let j = 0;
110
+ while (i < l && nth < l) {
111
+ const item = arr[i];
112
+ const { localName: itemLocalName, prefix: itemPrefix } = item;
113
+ if (itemLocalName === localName && itemPrefix === prefix) {
114
+ if (j === nth) {
115
+ res.add(item);
116
+ nth += a;
117
+ }
118
+ j++;
119
+ }
120
+ i++;
121
+ }
122
+ }
123
+ }
124
+ return [...res];
125
+ };
126
+
127
+ /**
128
+ * match type selector
129
+ * @param {object} leaf - ast leaf
130
+ * @param {object} node - element node
131
+ * @returns {?object} - matched node
132
+ */
133
+ const matchTypeSelector = (leaf = {}, node = {}) => {
134
+ const { name: leafName, type: leafType } = leaf;
135
+ const { localName, nodeType, prefix } = node;
136
+ let res;
137
+ if (leafType === TYPE_SELECTOR && nodeType === ELEMENT_NODE) {
138
+ // namespaced
139
+ if (/\|/.test(leafName)) {
140
+ const [leafPrefix, leafLocalName] = leafName.split('|');
141
+ if (((leafPrefix === '' && !prefix) || // |E
142
+ (leafPrefix === '*') || // *|E
143
+ (leafPrefix === prefix)) && // ns|E
144
+ (leafLocalName === '*' || leafLocalName === localName)) {
145
+ res = node;
146
+ }
147
+ } else if (leafName === '*' || leafName === localName) {
148
+ res = node;
149
+ }
150
+ }
151
+ return res || null;
152
+ };
153
+
154
+ /**
155
+ * match class selector
156
+ * @param {object} leaf - ast leaf
157
+ * @param {object} node - element node
158
+ * @returns {?object} - matched node
159
+ */
160
+ const matchClassSelector = (leaf = {}, node = {}) => {
161
+ const { name: leafName, type: leafType } = leaf;
162
+ const { classList, nodeType } = node;
163
+ let res;
164
+ if (leafType === CLASS_SELECTOR && nodeType === ELEMENT_NODE &&
165
+ classList.contains(leafName)) {
166
+ res = node;
167
+ }
168
+ return res || null;
169
+ };
170
+
171
+ /**
172
+ * match ID selector
173
+ * @param {object} leaf - ast leaf
174
+ * @param {object} node - element node
175
+ * @returns {?object} - matched node
176
+ */
177
+ const matchIdSelector = (leaf = {}, node = {}) => {
178
+ const { name: leafName, type: leafType } = leaf;
179
+ const { id, nodeType } = node;
180
+ let res;
181
+ if (leafType === ID_SELECTOR && nodeType === ELEMENT_NODE &&
182
+ leafName === id) {
183
+ res = node;
184
+ }
185
+ return res || null;
186
+ };
187
+
188
+ /**
189
+ * match attribute selector
190
+ * @param {object} leaf - ast leaf
191
+ * @param {object} node - element node
192
+ * @returns {?object} - matched node
193
+ */
194
+ const matchAttributeSelector = (leaf = {}, node = {}) => {
195
+ const {
196
+ flags: leafFlags, matcher: leafMatcher, name: leafName, type: leafType,
197
+ value: leafValue
198
+ } = leaf;
199
+ const { attributes, nodeType } = node;
200
+ let res;
201
+ if (leafType === ATTRIBUTE_SELECTOR && nodeType === ELEMENT_NODE &&
202
+ attributes?.length) {
203
+ const { name: leafAttrName } = leafName;
204
+ const caseInsensitive = !(leafFlags && /^s$/i.test(leafFlags));
205
+ const attrValues = [];
206
+ const l = attributes.length;
207
+ // namespaced
208
+ if (/\|/.test(leafAttrName)) {
209
+ const [leafAttrPrefix, leafAttrLocalName] = leafAttrName.split('|');
210
+ let i = 0;
211
+ while (i < l) {
212
+ const { name: itemName, value: itemValue } = attributes.item(i);
213
+ switch (leafAttrPrefix) {
214
+ case '':
215
+ if (leafAttrLocalName === itemName) {
216
+ if (caseInsensitive) {
217
+ attrValues.push(itemValue.toLowerCase());
218
+ } else {
219
+ attrValues.push(itemValue);
220
+ }
221
+ }
222
+ break;
223
+ case '*':
224
+ if (/:/.test(itemName)) {
225
+ if (itemName.endsWith(`:${leafAttrLocalName}`)) {
226
+ if (caseInsensitive) {
227
+ attrValues.push(itemValue.toLowerCase());
228
+ } else {
229
+ attrValues.push(itemValue);
230
+ }
231
+ }
232
+ } else if (leafAttrLocalName === itemName) {
233
+ if (caseInsensitive) {
234
+ attrValues.push(itemValue.toLowerCase());
235
+ } else {
236
+ attrValues.push(itemValue);
237
+ }
238
+ }
239
+ break;
240
+ default:
241
+ if (/:/.test(itemName)) {
242
+ const [itemNamePrefix, itemNameLocalName] = itemName.split(':');
243
+ if (leafAttrPrefix === itemNamePrefix &&
244
+ leafAttrLocalName === itemNameLocalName) {
245
+ if (caseInsensitive) {
246
+ attrValues.push(itemValue.toLowerCase());
247
+ } else {
248
+ attrValues.push(itemValue);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ i++;
254
+ }
255
+ } else {
256
+ let i = 0;
257
+ while (i < l) {
258
+ const { name: itemName, value: itemValue } = attributes.item(i);
259
+ if (/:/.test(itemName)) {
260
+ const [, itemNameLocalName] = itemName.split(':');
261
+ if (leafAttrName === itemNameLocalName) {
262
+ if (caseInsensitive) {
263
+ attrValues.push(itemValue.toLowerCase());
264
+ } else {
265
+ attrValues.push(itemValue);
266
+ }
267
+ }
268
+ } else if (leafAttrName === itemName) {
269
+ if (caseInsensitive) {
270
+ attrValues.push(itemValue.toLowerCase());
271
+ } else {
272
+ attrValues.push(itemValue);
273
+ }
274
+ }
275
+ i++;
276
+ }
277
+ }
278
+ if (attrValues.length) {
279
+ const {
280
+ name: leafAttrIdentValue, value: leafAttrStringValue
281
+ } = leafValue || {};
282
+ let attrValue;
283
+ if (leafAttrIdentValue) {
284
+ if (caseInsensitive) {
285
+ attrValue = leafAttrIdentValue.toLowerCase();
286
+ } else {
287
+ attrValue = leafAttrIdentValue;
288
+ }
289
+ } else if (leafAttrStringValue) {
290
+ if (caseInsensitive) {
291
+ attrValue = leafAttrStringValue.toLowerCase();
292
+ } else {
293
+ attrValue = leafAttrStringValue;
294
+ }
295
+ }
296
+ switch (leafMatcher) {
297
+ case null:
298
+ res = node;
299
+ break;
300
+ case '=':
301
+ if (attrValue && attrValues.includes(attrValue)) {
302
+ res = node;
303
+ }
304
+ break;
305
+ case '~=':
306
+ if (attrValue) {
307
+ for (const item of attrValues) {
308
+ const arr = item.split(/\s+/);
309
+ if (arr.includes(attrValue)) {
310
+ res = node;
311
+ break;
312
+ }
313
+ }
314
+ }
315
+ break;
316
+ case '|=':
317
+ if (attrValue) {
318
+ for (const item of attrValues) {
319
+ if (item === attrValue || item.startsWith(`${attrValue}-`)) {
320
+ res = node;
321
+ break;
322
+ }
323
+ }
324
+ }
325
+ break;
326
+ case '^=':
327
+ if (attrValue) {
328
+ for (const item of attrValues) {
329
+ if (item.startsWith(`${attrValue}`)) {
330
+ res = node;
331
+ break;
332
+ }
333
+ }
334
+ }
335
+ break;
336
+ case '$=':
337
+ if (attrValue) {
338
+ for (const item of attrValues) {
339
+ if (item.endsWith(`${attrValue}`)) {
340
+ res = node;
341
+ break;
342
+ }
343
+ }
344
+ }
345
+ break;
346
+ case '*=':
347
+ if (attrValue) {
348
+ for (const item of attrValues) {
349
+ if (item.includes(`${attrValue}`)) {
350
+ res = node;
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ break;
356
+ default:
357
+ console.warn(`Unknown matcher ${leafMatcher}`);
358
+ }
359
+ }
360
+ }
361
+ return res || null;
362
+ };
363
+
364
+ /**
365
+ * match An+B
366
+ * @param {string} leafName - leaf name
367
+ * @param {object} leaf - ast leaf
368
+ * @param {object} node - element node
369
+ * @returns {Array.<object|undefined>} - collection of matched nodes
370
+ */
371
+ const matchAnPlusB = (leafName, leaf = {}, node = {}) => {
372
+ const res = new Set();
373
+ if (typeof leafName === 'string') {
374
+ leafName = leafName.trim();
375
+ if (REG_PSEUDO_NTH.test(leafName)) {
376
+ const {
377
+ nth: {
378
+ a,
379
+ b,
380
+ name: identName
381
+ },
382
+ selector: leafSelector,
383
+ type: leafType
384
+ } = leaf;
385
+ const { nodeType } = node;
386
+ if (leafType === NTH && nodeType === ELEMENT_NODE) {
387
+ /*
388
+ // FIXME:
389
+ // :nth-child(An+B of S)
390
+ if (leafSelector) {
391
+ }
392
+ */
393
+ if (!leafSelector) {
394
+ const optMap = new Map();
395
+ if (identName) {
396
+ if (identName === 'even') {
397
+ optMap.set('a', 2);
398
+ optMap.set('b', 0);
399
+ } else if (identName === 'odd') {
400
+ optMap.set('a', 2);
401
+ optMap.set('b', 1);
402
+ }
403
+ if (/last/.test(leafName)) {
404
+ optMap.set('reverse', true);
405
+ }
406
+ } else {
407
+ if (typeof a === 'string' && /-?\d+/.test(a)) {
408
+ optMap.set('a', a * 1);
409
+ } else {
410
+ optMap.set('a', 0);
411
+ }
412
+ if (typeof b === 'string' && /-?\d+/.test(b)) {
413
+ optMap.set('b', b * 1);
414
+ } else {
415
+ optMap.set('b', 0);
416
+ }
417
+ if (/last/.test(leafName)) {
418
+ optMap.set('reverse', true);
419
+ }
420
+ }
421
+ if (optMap.size > 1) {
422
+ const opt = Object.fromEntries(optMap);
423
+ if (/^nth-(?:last-)?child$/.test(leafName)) {
424
+ const arr = collectNthChild(node, opt);
425
+ if (arr.length) {
426
+ for (const i of arr) {
427
+ res.add(i);
428
+ }
429
+ }
430
+ } else if (/^nth-(?:last-)?of-type$/.test(leafName)) {
431
+ const arr = collectNthOfType(node, opt);
432
+ if (arr.length) {
433
+ for (const i of arr) {
434
+ res.add(i);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ return [...res];
444
+ };
445
+
446
+ /**
447
+ * match language pseudo class
448
+ * @see https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1
449
+ * @param {object} leaf - ast leaf
450
+ * @param {object} node - element node
451
+ * @returns {?object} - matched node
452
+ */
453
+ const matchLanguagePseudoClass = (leaf = {}, node = {}) => {
454
+ const { name: leafName, type: leafType } = leaf;
455
+ const { lang, nodeType } = node;
456
+ let res;
457
+ if (leafType === IDENTIFIER && nodeType === ELEMENT_NODE) {
458
+ // FIXME:
459
+ /*
460
+ if (leafName === '') {
461
+ if (!lang) {
462
+ res = node;
463
+ }
464
+ } else if (leafName === '*') {
465
+ }
466
+ */
467
+ if (/[A-Za-z\d-]+/.test(leafName)) {
468
+ const codePart = '(?:-[A-Za-z\\d]+)?';
469
+ let reg;
470
+ if (/-/.test(leafName)) {
471
+ const [langMain, langSub, ...langRest] = leafName.split('-');
472
+ const extendedMain = `${langMain}${codePart}`;
473
+ const extendedSub = `-${langSub}${codePart}`;
474
+ let extendedRest = '';
475
+ if (langRest.length) {
476
+ for (const i of langRest) {
477
+ extendedRest += `-${i}${codePart}`;
478
+ }
479
+ }
480
+ reg = new RegExp(`^${extendedMain}${extendedSub}${extendedRest}$`, 'i');
481
+ } else {
482
+ reg = new RegExp(`^${leafName}${codePart}$`, 'i');
483
+ }
484
+ if (lang) {
485
+ if (reg.test(lang)) {
486
+ res = node;
487
+ }
488
+ } else {
489
+ let target = node;
490
+ while (target.parentNode) {
491
+ if (reg.test(target.lang)) {
492
+ res = node;
493
+ break;
494
+ }
495
+ target = target.parentNode;
496
+ }
497
+ }
498
+ }
499
+ }
500
+ return res || null;
501
+ };
502
+
503
+ /**
504
+ * match pseudo class selector
505
+ * @param {object} leaf - ast leaf
506
+ * @param {object} node - element node
507
+ * @param {object} [refPoint] - reference point
508
+ * @returns {Array.<object|undefined>} - collection of matched nodes
509
+ */
510
+ const matchPseudoClassSelector = (
511
+ leaf = {},
512
+ node = {},
513
+ refPoint = {}
514
+ ) => {
515
+ const { children: leafChildren, name: leafName, type: leafType } = leaf;
516
+ const { nodeType, ownerDocument } = node;
517
+ const res = new Set();
518
+ if (leafType === PSEUDO_CLASS_SELECTOR && nodeType === ELEMENT_NODE) {
519
+ if (Array.isArray(leafChildren)) {
520
+ const [leafChildAst] = leafChildren;
521
+ // :nth-child(), :nth-last-child(), nth-of-type(), :nth-last-of-type()
522
+ if (REG_PSEUDO_NTH.test(leafName)) {
523
+ const arr = matchAnPlusB(leafName, leafChildAst, node);
524
+ if (arr.length) {
525
+ for (const i of arr) {
526
+ res.add(i);
527
+ }
528
+ }
529
+ } else {
530
+ switch (leafName) {
531
+ case 'dir':
532
+ if (leafChildAst.name === node.dir) {
533
+ res.add(node);
534
+ }
535
+ break;
536
+ case 'lang':
537
+ if (matchLanguagePseudoClass(leafChildAst, node)) {
538
+ res.add(node);
539
+ }
540
+ break;
541
+ case 'current':
542
+ case 'nth-col':
543
+ case 'nth-last-col':
544
+ console.warn(`Unsupported pseudo class ${leafName}`);
545
+ break;
546
+ default:
547
+ console.warn(`Unknown pseudo class ${leafName}`);
548
+ }
549
+ }
550
+ } else {
551
+ const root = ownerDocument.documentElement;
552
+ const docURL = new URL(ownerDocument.URL);
553
+ switch (leafName) {
554
+ case 'any-link':
555
+ case 'link':
556
+ // FIXME: what about namespaced href? e.g. xlink:href
557
+ if (node.hasAttribute('href')) {
558
+ res.add(node);
559
+ }
560
+ break;
561
+ case 'local-link':
562
+ // FIXME: what about namespaced href? e.g. xlink:href
563
+ if (node.hasAttribute('href')) {
564
+ const attrURL = new URL(node.getAttribute('href'), docURL.href);
565
+ if (attrURL.origin === docURL.origin &&
566
+ attrURL.pathname === docURL.pathname) {
567
+ res.add(node);
568
+ }
569
+ }
570
+ break;
571
+ case 'visited':
572
+ // prevent fingerprinting
573
+ break;
574
+ case 'target':
575
+ if (docURL.hash && node.id && docURL.hash === `#${node.id}`) {
576
+ res.add(node);
577
+ }
578
+ break;
579
+ case 'scope':
580
+ if (refPoint?.nodeType === ELEMENT_NODE) {
581
+ if (node === refPoint) {
582
+ res.add(node);
583
+ }
584
+ } else if (node === root) {
585
+ res.add(node);
586
+ }
587
+ break;
588
+ case 'focus':
589
+ if (node === ownerDocument.activeElement) {
590
+ res.add(node);
591
+ }
592
+ break;
593
+ case 'open':
594
+ if (node.hasAttribute('open')) {
595
+ res.add(node);
596
+ }
597
+ break;
598
+ case 'closed':
599
+ // FIXME: is this really okay?
600
+ if (!node.hasAttribute('open')) {
601
+ res.add(node);
602
+ }
603
+ break;
604
+ case 'disabled':
605
+ if (node.hasAttribute('disabled')) {
606
+ res.add(node);
607
+ }
608
+ break;
609
+ case 'enabled':
610
+ // FIXME: is this really okay?
611
+ if (!node.hasAttribute('disabled')) {
612
+ res.add(node);
613
+ }
614
+ break;
615
+ case 'checked':
616
+ if (node.checked) {
617
+ res.add(node);
618
+ }
619
+ break;
620
+ case 'required':
621
+ if (node.required) {
622
+ res.add(node);
623
+ }
624
+ break;
625
+ case 'optional':
626
+ // FIXME: is this really okay?
627
+ if (!node.required) {
628
+ res.add(node);
629
+ }
630
+ break;
631
+ case 'root':
632
+ if (node === root) {
633
+ res.add(node);
634
+ }
635
+ break;
636
+ case 'first-child':
637
+ if (node === node.parentNode.firstElementChild) {
638
+ res.add(node);
639
+ }
640
+ break;
641
+ case 'last-child':
642
+ if (node === node.parentNode.lastElementChild) {
643
+ res.add(node);
644
+ }
645
+ break;
646
+ case 'only-child':
647
+ if (node === node.parentNode.firstElementChild &&
648
+ node === node.parentNode.lastElementChild) {
649
+ res.add(node);
650
+ }
651
+ break;
652
+ case 'first-of-type': {
653
+ const [node1] = collectNthOfType(node, {
654
+ a: 0,
655
+ b: 0
656
+ });
657
+ if (node1) {
658
+ res.add(node1);
659
+ }
660
+ break;
661
+ }
662
+ case 'last-of-type': {
663
+ const [node1] = collectNthOfType(node, {
664
+ a: 0,
665
+ b: 0,
666
+ reverse: true
667
+ });
668
+ if (node1) {
669
+ res.add(node1);
670
+ }
671
+ break;
672
+ }
673
+ case 'only-of-type': {
674
+ const [node1] = collectNthOfType(node, {
675
+ a: 0,
676
+ b: 0
677
+ });
678
+ const [node2] = collectNthOfType(node, {
679
+ a: 0,
680
+ b: 0,
681
+ reverse: true
682
+ });
683
+ if (node1 === node && node2 === node) {
684
+ res.add(node);
685
+ }
686
+ break;
687
+ }
688
+ case 'active':
689
+ case 'autofill':
690
+ case 'blank':
691
+ case 'buffering':
692
+ case 'current':
693
+ case 'default':
694
+ case 'empty':
695
+ case 'focus-visible':
696
+ case 'focus-within':
697
+ case 'fullscreen':
698
+ case 'future':
699
+ case 'hover':
700
+ case 'indeterminate':
701
+ case 'invalid':
702
+ case 'in-range':
703
+ case 'modal':
704
+ case 'muted':
705
+ case 'out-of-range':
706
+ case 'past':
707
+ case 'paused':
708
+ case 'picture-in-picture':
709
+ case 'placeholder-shown':
710
+ case 'playing':
711
+ case 'read-only':
712
+ case 'read-write':
713
+ case 'seeking':
714
+ case 'stalled':
715
+ case 'target-within':
716
+ case 'user-invalid':
717
+ case 'user-valid':
718
+ case 'valid':
719
+ case 'volume-locked':
720
+ console.warn(`Unsupported pseudo class ${leafName}`);
721
+ break;
722
+ default:
723
+ console.warn(`Unknown pseudo class ${leafName}`);
724
+ }
725
+ }
726
+ }
727
+ return [...res];
728
+ };
729
+
730
+ /**
731
+ * Matcher
732
+ */
733
+ class Matcher {
734
+ /* private fields */
735
+ #ast;
736
+ #document;
737
+ #node;
738
+ #selector;
739
+
740
+ /**
741
+ * construct
742
+ * @param {string} selector - CSS selector
743
+ * @param {object} refPoint - reference point
744
+ */
745
+ constructor(selector, refPoint) {
746
+ this.#ast = parseSelector(selector);
747
+ this.#document = refPoint?.ownerDocument ?? refPoint;
748
+ this.#node = refPoint;
749
+ this.#selector = selector;
750
+ }
751
+
752
+ /**
753
+ * create iterator
754
+ * @param {object} ast - ast
755
+ * @param {object} root - root node
756
+ * @returns {object} - iterator
757
+ */
758
+ _createIterator(ast = this.#ast, root = this.#node) {
759
+ const iterator = this.#document.createNodeIterator(
760
+ root,
761
+ NodeFilter.SHOW_ELEMENT,
762
+ node => {
763
+ const arr = this._match(ast, node);
764
+ return arr.length ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
765
+ }
766
+ );
767
+ return iterator;
768
+ }
769
+
770
+ /**
771
+ * parse ast and run
772
+ * @param {object} ast - ast
773
+ * @param {object} node - element node
774
+ * @returns {Array.<object|undefined>} - collection of matched nodes
775
+ */
776
+ _parseAst(ast, node) {
777
+ const items = walkAst(ast);
778
+ const res = new Set();
779
+ if (items.length) {
780
+ for (const item of items) {
781
+ const arr = this._matchSelector(item, node);
782
+ if (arr.length) {
783
+ for (const i of arr) {
784
+ res.add(i);
785
+ }
786
+ }
787
+ }
788
+ }
789
+ return [...res];
790
+ }
791
+
792
+ /**
793
+ * match adjacent leaves
794
+ * @param {Array.<object>} leaves - array of ast leaves
795
+ * @param {object} node - element node
796
+ * @returns {?object} - matched node
797
+ */
798
+ _matchAdjacentLeaves(leaves, node) {
799
+ const [prevLeaf, nextLeaf] = leaves;
800
+ const iterator = this._createIterator(prevLeaf, node);
801
+ let prevNode = iterator.nextNode();
802
+ const nodes = new Set();
803
+ while (prevNode) {
804
+ const arr = this._match(prevLeaf, prevNode);
805
+ if (arr.length) {
806
+ for (const item of arr) {
807
+ const a = this._match(nextLeaf, item);
808
+ if (a.length) {
809
+ for (const i of a) {
810
+ nodes.add(i);
811
+ }
812
+ }
813
+ }
814
+ }
815
+ prevNode = iterator.nextNode();
816
+ }
817
+ const items = [...nodes];
818
+ let res;
819
+ for (const item of items) {
820
+ if (item === node) {
821
+ res = item;
822
+ break;
823
+ }
824
+ }
825
+ return res || null;
826
+ }
827
+
828
+ /**
829
+ * match combinator
830
+ * @param {Array.<object>} leaves - array of ast leaves
831
+ * @param {object} prevNode - element node
832
+ * @returns {Array.<object|undefined>} - matched nodes
833
+ */
834
+ _matchCombinator(leaves, prevNode) {
835
+ const [{ name: comboName }, ...items] = leaves;
836
+ const nodes = new Set();
837
+ if (items.length) {
838
+ const [firstItem] = items;
839
+ let rootNode = prevNode;
840
+ if (comboName === '+' || comboName === '~') {
841
+ rootNode = rootNode.parentNode;
842
+ }
843
+ const iterator = this._createIterator(firstItem, rootNode);
844
+ let nextNode = iterator.nextNode();
845
+ const item = items.shift();
846
+ while (nextNode) {
847
+ const arr = this._match(item, nextNode);
848
+ if (arr.length) {
849
+ for (const i of arr) {
850
+ nodes.add(i);
851
+ }
852
+ }
853
+ nextNode = iterator.nextNode();
854
+ }
855
+ while (items.length) {
856
+ const leaf = items.shift();
857
+ if (nodes.size) {
858
+ nodes.forEach(node => {
859
+ const arr = this._match(leaf, node);
860
+ if (!arr.length) {
861
+ nodes.delete(node);
862
+ }
863
+ });
864
+ }
865
+ }
866
+ }
867
+ const res = new Set();
868
+ if (nodes.size && /^[ >+~]$/.test(comboName)) {
869
+ const items = [...nodes];
870
+ for (const item of items) {
871
+ let refNode = item;
872
+ switch (comboName) {
873
+ case '>':
874
+ if (refNode.parentNode === prevNode) {
875
+ res.add(item);
876
+ }
877
+ break;
878
+ case '~':
879
+ refNode = refNode.previousElementSibling;
880
+ while (refNode) {
881
+ if (refNode === prevNode) {
882
+ res.add(item);
883
+ break;
884
+ }
885
+ refNode = refNode.previousElementSibling;
886
+ }
887
+ break;
888
+ case '+':
889
+ if (refNode.previousElementSibling === prevNode) {
890
+ res.add(item);
891
+ }
892
+ break;
893
+ default:
894
+ refNode = refNode.parentNode;
895
+ while (refNode) {
896
+ if (refNode === prevNode) {
897
+ res.add(item);
898
+ break;
899
+ }
900
+ refNode = refNode.parentNode;
901
+ }
902
+ }
903
+ }
904
+ }
905
+ return [...res];
906
+ }
907
+
908
+ /**
909
+ * match argument leaf
910
+ * @param {object} leaf - argument ast leaf
911
+ * @param {object} node - element node
912
+ * @returns {Array.<object|undefined>} - matched nodes
913
+ */
914
+ _matchArgumentLeaf(leaf, node) {
915
+ const iterator = this._createIterator(leaf, node);
916
+ let nextNode = iterator.nextNode();
917
+ const res = new Set();
918
+ while (nextNode) {
919
+ const arr = this._match(leaf, nextNode);
920
+ if (arr.length) {
921
+ for (const i of arr) {
922
+ res.add(i);
923
+ }
924
+ }
925
+ nextNode = iterator.nextNode();
926
+ }
927
+ return [...res];
928
+ }
929
+
930
+ /**
931
+ * match logical pseudo class functions - :is(), :has(), :not(), :where()
932
+ * @param {object} leaf - ast leaf
933
+ * @param {object} node - element node
934
+ * @returns {?object} - matched node
935
+ */
936
+ _matchLogicalPseudoFunc(leaf, node) {
937
+ const ast = walkAst(leaf);
938
+ let res;
939
+ if (ast.length) {
940
+ const { name: leafName } = leaf;
941
+ switch (leafName) {
942
+ // :has()
943
+ case 'has': {
944
+ let matched;
945
+ for (const items of ast) {
946
+ const item = items.shift();
947
+ const { type: itemType } = item;
948
+ const itemLeaves = [];
949
+ let firstItem = item;
950
+ if (itemType !== COMBINATOR) {
951
+ const comboLeaf = {
952
+ name: ' ',
953
+ type: COMBINATOR
954
+ };
955
+ itemLeaves.push(comboLeaf, firstItem);
956
+ } else {
957
+ firstItem = items.shift();
958
+ itemLeaves.push(item, firstItem);
959
+ }
960
+ const arr = this._matchCombinator(itemLeaves, node);
961
+ if (arr.length) {
962
+ matched = true;
963
+ while (items.length && matched) {
964
+ const adjacentLeaves = [firstItem, items.shift()];
965
+ matched = this._matchAdjacentLeaves(adjacentLeaves, node);
966
+ }
967
+ break;
968
+ }
969
+ }
970
+ if (matched) {
971
+ res = node;
972
+ }
973
+ break;
974
+ }
975
+ // :not()
976
+ case 'not': {
977
+ let matched;
978
+ for (const items of ast) {
979
+ const item = items.shift();
980
+ const arr = this._matchArgumentLeaf(item, node);
981
+ if (arr.length) {
982
+ matched = true;
983
+ while (items.length && matched) {
984
+ const adjacentLeaves = [item, items.shift()];
985
+ matched = this._matchAdjacentLeaves(adjacentLeaves, node);
986
+ }
987
+ break;
988
+ }
989
+ }
990
+ if (!matched) {
991
+ res = node;
992
+ }
993
+ break;
994
+ }
995
+ // :is(), :where()
996
+ default: {
997
+ let matched;
998
+ for (const items of ast) {
999
+ const item = items.shift();
1000
+ const arr = this._matchArgumentLeaf(item, node);
1001
+ if (arr.length) {
1002
+ matched = true;
1003
+ while (items.length && matched) {
1004
+ const adjacentLeaves = [item, items.shift()];
1005
+ matched = this._matchAdjacentLeaves(adjacentLeaves, node);
1006
+ }
1007
+ break;
1008
+ }
1009
+ }
1010
+ if (matched) {
1011
+ res = node;
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+ return res || null;
1017
+ }
1018
+
1019
+ /**
1020
+ * match selector
1021
+ * @param {Array.<object>} children - selector children
1022
+ * @param {object} node - element node
1023
+ * @returns {Array.<object|undefined>} - collection of matched nodes
1024
+ */
1025
+ _matchSelector(children, node) {
1026
+ const res = new Set();
1027
+ if (Array.isArray(children) && children.length) {
1028
+ const [firstChild] = children;
1029
+ let iteratorLeaf;
1030
+ if (firstChild.type === COMBINATOR ||
1031
+ (firstChild.type === PSEUDO_CLASS_SELECTOR &&
1032
+ REG_PSEUDO_NTH.test(firstChild.name))) {
1033
+ iteratorLeaf = {
1034
+ name: '*',
1035
+ type: TYPE_SELECTOR
1036
+ };
1037
+ } else {
1038
+ iteratorLeaf = children.shift();
1039
+ }
1040
+ const iterator = this._createIterator(iteratorLeaf, node);
1041
+ let nextNode = iterator.nextNode();
1042
+ while (nextNode) {
1043
+ const [...items] = children;
1044
+ if (items.length) {
1045
+ if (items.length === 1) {
1046
+ const item = items.shift();
1047
+ const { name: itemName, type: itemType } = item;
1048
+ if (itemType === PSEUDO_CLASS_SELECTOR &&
1049
+ REG_PSEUDO_FUNC.test(itemName)) {
1050
+ nextNode = this._matchLogicalPseudoFunc(item, nextNode);
1051
+ if (nextNode) {
1052
+ res.add(nextNode);
1053
+ nextNode = null;
1054
+ }
1055
+ } else {
1056
+ const arr = this._match(item, nextNode);
1057
+ if (arr.length) {
1058
+ for (const i of arr) {
1059
+ res.add(i);
1060
+ }
1061
+ }
1062
+ }
1063
+ } else {
1064
+ do {
1065
+ const item = items.shift();
1066
+ const { name: itemName, type: itemType } = item;
1067
+ if (itemType === PSEUDO_CLASS_SELECTOR &&
1068
+ REG_PSEUDO_FUNC.test(itemName)) {
1069
+ nextNode = this._matchLogicalPseudoFunc(item, nextNode);
1070
+ } else if (itemType === COMBINATOR) {
1071
+ const leaves = [];
1072
+ leaves.push(item);
1073
+ while (items.length) {
1074
+ const [nextItem] = items;
1075
+ if (nextItem.type === COMBINATOR ||
1076
+ (nextItem.type === PSEUDO_CLASS_SELECTOR &&
1077
+ REG_PSEUDO_NTH.test(nextItem.name)) ||
1078
+ (nextItem.type === PSEUDO_CLASS_SELECTOR &&
1079
+ REG_PSEUDO_FUNC.test(nextItem.name))) {
1080
+ break;
1081
+ } else {
1082
+ leaves.push(items.shift());
1083
+ }
1084
+ }
1085
+ const arr = this._matchCombinator(leaves, nextNode);
1086
+ if (!arr.length || arr.length === 1) {
1087
+ [nextNode] = arr;
1088
+ } else {
1089
+ if (items.length) {
1090
+ for (const i of arr) {
1091
+ const a = this._matchSelector(items, i);
1092
+ if (a.length) {
1093
+ for (const j of a) {
1094
+ res.add(j);
1095
+ }
1096
+ }
1097
+ }
1098
+ } else {
1099
+ for (const i of arr) {
1100
+ res.add(i);
1101
+ }
1102
+ }
1103
+ nextNode = null;
1104
+ }
1105
+ } else {
1106
+ [nextNode] = this._match(item, nextNode);
1107
+ }
1108
+ } while (items.length && nextNode);
1109
+ if (nextNode) {
1110
+ res.add(nextNode);
1111
+ }
1112
+ }
1113
+ } else if (nextNode) {
1114
+ res.add(nextNode);
1115
+ }
1116
+ nextNode = iterator.nextNode();
1117
+ }
1118
+ }
1119
+ return [...res];
1120
+ }
1121
+
1122
+ /**
1123
+ * match ast and node
1124
+ * @param {object} [ast] - ast tree
1125
+ * @param {object} [node] - element node
1126
+ * @returns {Array.<object|undefined>} - collection of matched nodes
1127
+ */
1128
+ _match(ast = this.#ast, node = this.#node) {
1129
+ const res = new Set();
1130
+ const { name, type } = ast;
1131
+ switch (type) {
1132
+ case TYPE_SELECTOR:
1133
+ if (matchTypeSelector(ast, node)) {
1134
+ res.add(node);
1135
+ }
1136
+ break;
1137
+ case CLASS_SELECTOR:
1138
+ if (matchClassSelector(ast, node)) {
1139
+ res.add(node);
1140
+ }
1141
+ break;
1142
+ case ID_SELECTOR:
1143
+ if (matchIdSelector(ast, node)) {
1144
+ res.add(node);
1145
+ }
1146
+ break;
1147
+ case ATTRIBUTE_SELECTOR:
1148
+ if (matchAttributeSelector(ast, node)) {
1149
+ res.add(node);
1150
+ }
1151
+ break;
1152
+ case PSEUDO_CLASS_SELECTOR:
1153
+ if (!REG_PSEUDO_FUNC.test(name)) {
1154
+ const arr = matchPseudoClassSelector(ast, node, this.#node);
1155
+ if (arr.length) {
1156
+ for (const i of arr) {
1157
+ res.add(i);
1158
+ }
1159
+ }
1160
+ }
1161
+ break;
1162
+ default: {
1163
+ const arr = this._parseAst(ast, node);
1164
+ if (arr.length) {
1165
+ for (const i of arr) {
1166
+ res.add(i);
1167
+ }
1168
+ }
1169
+ }
1170
+ }
1171
+ return [...res];
1172
+ }
1173
+
1174
+ /**
1175
+ * matches
1176
+ * @returns {boolean} - matched node
1177
+ */
1178
+ matches() {
1179
+ const arr = this._match(this.#ast, this.#document);
1180
+ const node = this.#node;
1181
+ let res;
1182
+ if (arr.length) {
1183
+ for (const i of arr) {
1184
+ if (i === node) {
1185
+ res = true;
1186
+ break;
1187
+ }
1188
+ }
1189
+ }
1190
+ return !!res;
1191
+ }
1192
+
1193
+ /**
1194
+ * closest
1195
+ * @returns {?object} - matched node
1196
+ */
1197
+ closest() {
1198
+ const arr = this._match(this.#ast, this.#document);
1199
+ let node = this.#node;
1200
+ let res;
1201
+ while (node) {
1202
+ for (const i of arr) {
1203
+ if (i === node) {
1204
+ res = i;
1205
+ break;
1206
+ }
1207
+ }
1208
+ if (res) {
1209
+ break;
1210
+ }
1211
+ node = node.parentNode;
1212
+ }
1213
+ return res || null;
1214
+ }
1215
+
1216
+ /**
1217
+ * query selector
1218
+ * @returns {?object} - matched node
1219
+ */
1220
+ querySelector() {
1221
+ const arr = this._match(this.#ast, this.#node);
1222
+ let res;
1223
+ if (arr.length) {
1224
+ [res] = arr;
1225
+ }
1226
+ return res || null;
1227
+ }
1228
+
1229
+ /**
1230
+ * query selector all
1231
+ * NOTE: returns Array, not NodeList
1232
+ * @returns {Array.<object|undefined>} - collection of matched nodes
1233
+ */
1234
+ querySelectorAll() {
1235
+ const arr = this._match(this.#ast, this.#node);
1236
+ const res = new Set();
1237
+ if (arr.length) {
1238
+ for (const i of arr) {
1239
+ res.add(i);
1240
+ }
1241
+ }
1242
+ return [...res];
1243
+ }
1244
+ };
1245
+
1246
+ module.exports = {
1247
+ Matcher,
1248
+ collectNthChild,
1249
+ collectNthOfType,
1250
+ matchAnPlusB,
1251
+ matchAttributeSelector,
1252
+ matchClassSelector,
1253
+ matchIdSelector,
1254
+ matchLanguagePseudoClass,
1255
+ matchPseudoClassSelector,
1256
+ matchTypeSelector
1257
+ };