@markuplint/selector 4.7.7 → 5.0.0-alpha.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/lib/selector.js CHANGED
@@ -1,15 +1,3 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _Selector_ruleset, _Ruleset_selectorGroup, _StructuredSelector_edge, _StructuredSelector_selector, _SelectorTarget_combinedFrom, _SelectorTarget_extended, _SelectorTarget_isAdded;
13
1
  import { resolveNamespace } from '@markuplint/ml-spec';
14
2
  import parser from 'postcss-selector-parser';
15
3
  import { compareSpecificity } from './compare-specificity.js';
@@ -18,16 +6,28 @@ import { InvalidSelectorError } from './invalid-selector-error.js';
18
6
  import { isElement, isNonDocumentTypeChildNode, isPureHTMLElement } from './is.js';
19
7
  const selLog = coreLog.extend('selector');
20
8
  const resLog = coreLog.extend('result');
9
+ /**
10
+ * CSS selector matcher that parses a selector string and matches it against nodes.
11
+ *
12
+ * Use {@link createSelector} to create cached instances with extended pseudo-class support.
13
+ */
21
14
  export class Selector {
15
+ #ruleset;
16
+ /**
17
+ * @param selector - The CSS selector string to parse
18
+ * @param extended - Extended pseudo-class handlers to register
19
+ */
22
20
  constructor(selector, extended = {}) {
23
- _Selector_ruleset.set(this, void 0);
24
- __classPrivateFieldSet(this, _Selector_ruleset, Ruleset.parse(selector, extended), "f");
21
+ this.#ruleset = Ruleset.parse(selector, extended);
25
22
  }
26
- match(
27
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
28
- el,
29
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
30
- scope) {
23
+ /**
24
+ * Tests whether the given node matches this selector.
25
+ *
26
+ * @param el - The node to test
27
+ * @param scope - The scope node for `:scope` pseudo-class resolution
28
+ * @returns The specificity of the first matching selector, or `false` if none matched
29
+ */
30
+ match(el, scope) {
31
31
  scope = scope ?? (isElement(el) ? el : null);
32
32
  const results = this.search(el, scope);
33
33
  for (const result of results) {
@@ -37,16 +37,19 @@ export class Selector {
37
37
  }
38
38
  return false;
39
39
  }
40
- search(
41
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
42
- el,
43
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
44
- scope) {
40
+ /**
41
+ * Evaluates all comma-separated selectors against the given node
42
+ * and returns each result (matched or unmatched).
43
+ *
44
+ * @param el - The node to test
45
+ * @param scope - The scope node for `:scope` pseudo-class resolution
46
+ * @returns An array of results, one per comma-separated selector alternative
47
+ */
48
+ search(el, scope) {
45
49
  scope = scope ?? (isElement(el) ? el : null);
46
- return __classPrivateFieldGet(this, _Selector_ruleset, "f").match(el, scope);
50
+ return this.#ruleset.match(el, scope);
47
51
  }
48
52
  }
49
- _Selector_ruleset = new WeakMap();
50
53
  class Ruleset {
51
54
  static parse(selector, extended) {
52
55
  const selectors = [];
@@ -63,27 +66,24 @@ class Ruleset {
63
66
  }
64
67
  return new Ruleset(selectors, extended, 0);
65
68
  }
69
+ headCombinator;
70
+ #selectorGroup = [];
66
71
  constructor(selectors, extended, depth) {
67
- _Ruleset_selectorGroup.set(this, []);
68
- __classPrivateFieldGet(this, _Ruleset_selectorGroup, "f").push(...selectors.map(selector => new StructuredSelector(selector, depth, extended)));
69
- const head = __classPrivateFieldGet(this, _Ruleset_selectorGroup, "f")[0];
72
+ this.#selectorGroup.push(...selectors.map(selector => new StructuredSelector(selector, depth, extended)));
73
+ const head = this.#selectorGroup[0];
70
74
  this.headCombinator = head?.headCombinator ?? null;
71
75
  if (this.headCombinator && depth <= 0) {
72
- if (__classPrivateFieldGet(this, _Ruleset_selectorGroup, "f")[0]?.selector) {
73
- throw new InvalidSelectorError(__classPrivateFieldGet(this, _Ruleset_selectorGroup, "f")[0]?.selector);
76
+ if (this.#selectorGroup[0]?.selector) {
77
+ throw new InvalidSelectorError(this.#selectorGroup[0]?.selector);
74
78
  }
75
79
  throw new Error('Combinated selector depth is not expected');
76
80
  }
77
81
  }
78
- match(
79
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
80
- el,
81
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
82
- scope) {
82
+ match(el, scope) {
83
83
  if (coreLog.enabled) {
84
84
  coreLog('<%s> (%s)', isElement(el) ? el.localName : el.nodeName, scope ? (isElement(scope) ? scope.localName : scope.nodeName) : null);
85
85
  }
86
- return __classPrivateFieldGet(this, _Ruleset_selectorGroup, "f").map(selector => {
86
+ return this.#selectorGroup.map(selector => {
87
87
  if (selLog.enabled) {
88
88
  selLog('"%s"', selector.selector);
89
89
  }
@@ -95,16 +95,16 @@ class Ruleset {
95
95
  });
96
96
  }
97
97
  }
98
- _Ruleset_selectorGroup = new WeakMap();
99
98
  class StructuredSelector {
99
+ #edge;
100
+ headCombinator;
101
+ #selector;
100
102
  constructor(selector, depth, extended) {
101
- _StructuredSelector_edge.set(this, void 0);
102
- _StructuredSelector_selector.set(this, void 0);
103
- __classPrivateFieldSet(this, _StructuredSelector_selector, selector, "f");
104
- __classPrivateFieldSet(this, _StructuredSelector_edge, new SelectorTarget(extended, depth), "f");
103
+ this.#selector = selector;
104
+ this.#edge = new SelectorTarget(extended, depth);
105
105
  this.headCombinator =
106
- __classPrivateFieldGet(this, _StructuredSelector_selector, "f").nodes[0]?.type === 'combinator' ? (__classPrivateFieldGet(this, _StructuredSelector_selector, "f").nodes[0].value ?? null) : null;
107
- const nodes = [...__classPrivateFieldGet(this, _StructuredSelector_selector, "f").nodes];
106
+ this.#selector.nodes[0]?.type === 'combinator' ? (this.#selector.nodes[0].value ?? null) : null;
107
+ const nodes = [...this.#selector.nodes];
108
108
  if (0 < depth && this.headCombinator) {
109
109
  nodes.unshift(parser.pseudo({ value: ':scope' }));
110
110
  }
@@ -112,8 +112,8 @@ class StructuredSelector {
112
112
  switch (node.type) {
113
113
  case 'combinator': {
114
114
  const combinedTarget = new SelectorTarget(extended, depth);
115
- combinedTarget.from(__classPrivateFieldGet(this, _StructuredSelector_edge, "f"), node);
116
- __classPrivateFieldSet(this, _StructuredSelector_edge, combinedTarget, "f");
115
+ combinedTarget.from(this.#edge, node);
116
+ this.#edge = combinedTarget;
117
117
  break;
118
118
  }
119
119
  case 'root':
@@ -127,38 +127,34 @@ class StructuredSelector {
127
127
  throw new Error(`Unsupported comment in selector: ${selector.toString()}`);
128
128
  }
129
129
  default: {
130
- __classPrivateFieldGet(this, _StructuredSelector_edge, "f").add(node);
130
+ this.#edge.add(node);
131
131
  }
132
132
  }
133
133
  }
134
134
  }
135
135
  get selector() {
136
- return __classPrivateFieldGet(this, _StructuredSelector_selector, "f").nodes.join('');
136
+ return this.#selector.nodes.join('');
137
137
  }
138
- match(
139
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
140
- el,
141
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
142
- scope) {
143
- return __classPrivateFieldGet(this, _StructuredSelector_edge, "f").match(el, scope, 0);
138
+ match(el, scope) {
139
+ return this.#edge.match(el, scope, 0);
144
140
  }
145
141
  }
146
- _StructuredSelector_edge = new WeakMap(), _StructuredSelector_selector = new WeakMap();
147
142
  class SelectorTarget {
143
+ attr = [];
144
+ class = [];
145
+ #combinedFrom = null;
146
+ depth;
147
+ #extended;
148
+ id = [];
149
+ #isAdded = false;
150
+ pseudo = [];
151
+ tag = null;
148
152
  constructor(extended, depth) {
149
- this.attr = [];
150
- this.class = [];
151
- _SelectorTarget_combinedFrom.set(this, null);
152
- _SelectorTarget_extended.set(this, void 0);
153
- this.id = [];
154
- _SelectorTarget_isAdded.set(this, false);
155
- this.pseudo = [];
156
- this.tag = null;
157
- __classPrivateFieldSet(this, _SelectorTarget_extended, extended, "f");
153
+ this.#extended = extended;
158
154
  this.depth = depth;
159
155
  }
160
156
  add(selector) {
161
- __classPrivateFieldSet(this, _SelectorTarget_isAdded, true, "f");
157
+ this.#isAdded = true;
162
158
  switch (selector.type) {
163
159
  case 'tag':
164
160
  case 'universal': {
@@ -184,17 +180,13 @@ class SelectorTarget {
184
180
  }
185
181
  }
186
182
  from(target, combinator) {
187
- __classPrivateFieldSet(this, _SelectorTarget_combinedFrom, { target, combinator }, "f");
183
+ this.#combinedFrom = { target, combinator };
188
184
  }
189
- match(
190
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
191
- el,
192
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
193
- scope, count) {
185
+ match(el, scope, count) {
194
186
  const result = this._match(el, scope, count);
195
187
  if (selLog.enabled) {
196
188
  const nodeName = el.nodeName;
197
- const selector = __classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f")?.target.toString() ?? this.toString();
189
+ const selector = this.#combinedFrom?.target.toString() ?? this.toString();
198
190
  const combinator = result.combinator ? ` ${result.combinator}` : '';
199
191
  selLog('The %s element by "%s" => %s (%d)', nodeName, `${selector}${combinator}`, result.matched, count);
200
192
  if (selector === ':scope') {
@@ -213,22 +205,18 @@ class SelectorTarget {
213
205
  this.pseudo.map(pseudo => pseudo.value).join(''),
214
206
  ].join('');
215
207
  }
216
- _match(
217
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
218
- el,
219
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
220
- scope, count) {
208
+ _match(el, scope, count) {
221
209
  const unitCheck = this._matchWithoutCombineChecking(el, scope);
222
210
  if (!unitCheck.matched) {
223
211
  return unitCheck;
224
212
  }
225
- if (!__classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f")) {
213
+ if (!this.#combinedFrom) {
226
214
  return unitCheck;
227
215
  }
228
216
  if (!isNonDocumentTypeChildNode(el)) {
229
217
  return unitCheck;
230
218
  }
231
- const { target, combinator } = __classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f");
219
+ const { target, combinator } = this.#combinedFrom;
232
220
  switch (combinator.value) {
233
221
  // Descendant combinator
234
222
  case ' ': {
@@ -424,7 +412,7 @@ class SelectorTarget {
424
412
  throw new Error('Unsupported column combinator yet. If you want it, please request it as the issue (https://github.com/markuplint/markuplint/issues/new).');
425
413
  }
426
414
  default: {
427
- throw new Error(`Unsupported ${__classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f").combinator.value} combinator in selector`);
415
+ throw new Error(`Unsupported ${this.#combinedFrom.combinator.value} combinator in selector`);
428
416
  }
429
417
  }
430
418
  }
@@ -436,11 +424,7 @@ class SelectorTarget {
436
424
  * @param scope
437
425
  * @private
438
426
  */
439
- _matchWithoutCombineChecking(
440
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
441
- el,
442
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
443
- scope) {
427
+ _matchWithoutCombineChecking(el, scope) {
444
428
  const specificity = [0, 0, 0];
445
429
  if (!isElement(el)) {
446
430
  return {
@@ -472,7 +456,7 @@ class SelectorTarget {
472
456
  }
473
457
  }
474
458
  let matched = true;
475
- if (!__classPrivateFieldGet(this, _SelectorTarget_isAdded, "f") && !isScope(el, scope)) {
459
+ if (!this.#isAdded && !isScope(el, scope)) {
476
460
  matched = false;
477
461
  }
478
462
  if (matched && !this.id.every(id => id.value === el.id)) {
@@ -501,7 +485,7 @@ class SelectorTarget {
501
485
  specificity[1] += this.attr.length;
502
486
  if (matched) {
503
487
  for (const pseudo of this.pseudo) {
504
- const pseudoRes = pseudoMatch(pseudo, el, scope, __classPrivateFieldGet(this, _SelectorTarget_extended, "f"), this.depth);
488
+ const pseudoRes = pseudoMatch(pseudo, el, scope, this.#extended, this.depth);
505
489
  specificity[0] += pseudoRes.specificity[0];
506
490
  specificity[1] += pseudoRes.specificity[1];
507
491
  specificity[2] += pseudoRes.specificity[2];
@@ -517,19 +501,18 @@ class SelectorTarget {
517
501
  if (matched) {
518
502
  return {
519
503
  specificity,
520
- matched,
504
+ matched: true,
521
505
  nodes: [el],
522
506
  has,
523
507
  };
524
508
  }
525
509
  return {
526
510
  specificity,
527
- matched,
511
+ matched: false,
528
512
  not,
529
513
  };
530
514
  }
531
515
  }
532
- _SelectorTarget_combinedFrom = new WeakMap(), _SelectorTarget_extended = new WeakMap(), _SelectorTarget_isAdded = new WeakMap();
533
516
  function attrMatch(attr,
534
517
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
535
518
  el) {
@@ -594,9 +577,7 @@ el) {
594
577
  }
595
578
  function pseudoMatch(pseudo,
596
579
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
597
- el,
598
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
599
- scope, extended, depth) {
580
+ el, scope, extended, depth) {
600
581
  switch (pseudo.value) {
601
582
  //
602
583
  /**
@@ -803,9 +784,7 @@ scope, extended, depth) {
803
784
  }
804
785
  function isScope(
805
786
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
806
- el,
807
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
808
- scope) {
787
+ el, scope) {
809
788
  return el === scope || el.parentNode === null;
810
789
  }
811
790
  function getDescendants(
package/lib/types.d.ts CHANGED
@@ -1,24 +1,112 @@
1
+ /**
2
+ * Minimal attribute representation for selector matching.
3
+ * Pure data — no methods, no class instances.
4
+ *
5
+ * In Rust this maps directly to a struct.
6
+ */
7
+ export interface SelectorAttr {
8
+ readonly name: string;
9
+ readonly localName: string;
10
+ readonly value: string;
11
+ readonly namespaceURI: string | null;
12
+ }
13
+ /**
14
+ * Minimal node representation for selector matching.
15
+ * Pure data with tree-navigation references.
16
+ *
17
+ * In Rust this maps to a trait backed by arena indices.
18
+ */
19
+ export interface SelectorNode {
20
+ readonly nodeType: number;
21
+ readonly nodeName: string;
22
+ readonly parentNode: SelectorNode | null;
23
+ }
24
+ /**
25
+ * Minimal element representation for CSS selector matching.
26
+ * Captures **only** the properties the selector engine actually reads.
27
+ *
28
+ * DOM `Element` and `MLElement` both satisfy this interface,
29
+ * but plain objects can satisfy it too — enabling Rust interop
30
+ * and unit-testing without a full DOM.
31
+ *
32
+ * In Rust this maps to a struct + trait.
33
+ */
34
+ export interface SelectorElement extends SelectorNode {
35
+ readonly localName: string;
36
+ readonly id: string;
37
+ readonly namespaceURI: string | null;
38
+ readonly classList: {
39
+ contains(className: string): boolean;
40
+ };
41
+ readonly attributes: Iterable<SelectorAttr>;
42
+ readonly parentElement: SelectorElement | null;
43
+ readonly previousElementSibling: SelectorElement | null;
44
+ readonly nextElementSibling: SelectorElement | null;
45
+ readonly children: Iterable<SelectorElement>;
46
+ }
47
+ /**
48
+ * A CSS specificity tuple: `[id, class, type]`.
49
+ * Each component counts selectors of the corresponding category.
50
+ */
1
51
  export type Specificity = readonly [number, number, number];
52
+ /**
53
+ * The result of evaluating a parsed selector against a node.
54
+ */
2
55
  export type SelectorResult = SelectorMatchedResult | SelectorUnmatchedResult;
56
+ /**
57
+ * A successful selector match result, including the specificity,
58
+ * the matched nodes, and any `:has()` sub-match results.
59
+ */
3
60
  export type SelectorMatchedResult = {
61
+ /** The computed specificity of the matched selector */
4
62
  readonly specificity: Specificity;
5
63
  readonly matched: true;
6
- readonly nodes: readonly (Element | Text)[];
64
+ /** The elements that were matched */
65
+ readonly nodes: readonly SelectorElement[];
66
+ /** Results from `:has()` pseudo-class sub-matches */
7
67
  readonly has: readonly SelectorMatchedResult[];
8
68
  };
69
+ /**
70
+ * An unsuccessful selector match result.
71
+ */
9
72
  export type SelectorUnmatchedResult = {
73
+ /** The computed specificity of the unmatched selector */
10
74
  readonly specificity: Specificity;
11
75
  readonly matched: false;
76
+ /** Results from `:not()` pseudo-class sub-matches that did match */
12
77
  readonly not?: readonly SelectorMatchedResult[];
13
78
  };
79
+ /**
80
+ * A regex-based selector that matches elements by node name and/or attribute
81
+ * patterns using regular expressions. Supports combinators for chained matching.
82
+ */
14
83
  export type RegexSelector = RegexSelectorWithoutCombination & {
84
+ /** An optional chained selector with a combinator */
15
85
  readonly combination?: {
16
86
  readonly combinator: RegexSelectorCombinator;
17
87
  } & RegexSelector;
18
88
  };
89
+ /**
90
+ * CSS combinator types supported by regex selectors.
91
+ *
92
+ * - `' '` – Descendant combinator
93
+ * - `'>'` – Child combinator
94
+ * - `'+'` – Next-sibling combinator
95
+ * - `'~'` – Subsequent-sibling combinator
96
+ * - `':has(+)'`– Previous-sibling combinator (via `:has()`)
97
+ * - `':has(~)'`– Previous subsequent-sibling combinator (via `:has()`)
98
+ */
19
99
  export type RegexSelectorCombinator = ' ' | '>' | '+' | '~' | ':has(+)' | ':has(~)';
100
+ /**
101
+ * The non-combinatorial part of a regex selector.
102
+ * Matches against the element's node name, attribute name, and/or attribute value
103
+ * using regular expression patterns.
104
+ */
20
105
  export type RegexSelectorWithoutCombination = {
106
+ /** Regex pattern to match against the element's local name */
21
107
  readonly nodeName?: string;
108
+ /** Regex pattern to match against attribute names */
22
109
  readonly attrName?: string;
110
+ /** Regex pattern to match against attribute values */
23
111
  readonly attrValue?: string;
24
112
  };
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@markuplint/selector",
3
- "version": "4.7.7",
3
+ "version": "5.0.0-alpha.0",
4
4
  "description": "Extended W3C Selectors matcher",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
7
7
  "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
8
11
  "type": "module",
9
12
  "exports": {
10
13
  ".": {
@@ -24,15 +27,15 @@
24
27
  "clean": "tsc --build --clean tsconfig.build.json"
25
28
  },
26
29
  "dependencies": {
27
- "@markuplint/ml-spec": "4.10.1",
30
+ "@markuplint/ml-spec": "5.0.0-alpha.0",
28
31
  "@types/debug": "4.1.12",
29
32
  "debug": "4.4.3",
30
- "postcss-selector-parser": "7.1.0",
31
- "type-fest": "4.41.0"
33
+ "postcss-selector-parser": "7.1.1",
34
+ "type-fest": "5.4.4"
32
35
  },
33
36
  "devDependencies": {
34
- "@types/jsdom": "21.1.7",
35
- "jsdom": "26.1.0"
37
+ "@types/jsdom": "27.0.0",
38
+ "jsdom": "28.1.0"
36
39
  },
37
- "gitHead": "6213ea30269ef404f030e67bbcc7fc7443ec1060"
40
+ "gitHead": "13dcfc84ec83d87360c720e253383b60767e1b56"
38
41
  }