@markuplint/selector 4.7.8 → 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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  `@markuplint/selector` は markuplint のための拡張 [W3C Selectors Level 4](https://www.w3.org/TR/selectors-4/) マッチャーです。2 つの独立したマッチングシステムを提供します:
6
6
 
7
- 1. **CSS セレクタマッチング** -- `postcss-selector-parser` を使用して標準 CSS セレクタをパースし、DOM ノードに対して完全な詳細度追跡付きでマッチングします。
7
+ 1. **CSS セレクタマッチング** -- `postcss-selector-parser` を使用して標準 CSS セレクタをパースし、要素に対して完全な詳細度追跡付きでマッチングします。
8
8
  2. **Regex セレクタマッチング** -- ノード名と属性に対する正規表現パターンでマッチングし、キャプチャグループデータを抽出します。
9
9
 
10
10
  また、markuplint 固有の拡張擬似クラス(`:aria()`、`:role()`、`:model()`)を定義し、HTML/ARIA 仕様データをセレクタマッチングに統合します。
@@ -14,13 +14,13 @@
14
14
  ```
15
15
  src/
16
16
  ├── index.ts — エクスポートエントリーポイント
17
- ├── types.ts — 型定義(Specificity, SelectorResult, RegexSelector 等)
17
+ ├── types.ts — 型定義(SelectorElement, SelectorNode, SelectorAttr, Specificity, SelectorResult 等)
18
18
  ├── selector.ts — コア Selector/Ruleset/StructuredSelector/SelectorTarget クラス
19
19
  ├── create-selector.ts — インスタンスキャッシュと拡張擬似クラス登録を持つ Selector ファクトリ
20
20
  ├── match-selector.ts — CSS/Regex セレクタマッチング公開関数
21
21
  ├── compare-specificity.ts — 詳細度比較ユーティリティ
22
22
  ├── regex-selector-matches.ts — 正規表現パターンマッチングヘルパー
23
- ├── is.ts — DOM ノード型ガード
23
+ ├── is.ts — ノード型ガード(SelectorNode/SelectorElement)
24
24
  ├── invalid-selector-error.ts — 無効セレクタ用カスタムエラークラス
25
25
  ├── debug.ts — デバッグログ設定(debug パッケージ使用)
26
26
  └── extended-selector/
@@ -81,13 +81,13 @@ flowchart TD
81
81
  | モジュール | 役割 | 主要エクスポート |
82
82
  | ------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
83
83
  | `index.ts` | エントリーポイント | 全公開 API の再エクスポート |
84
- | `types.ts` | 型定義 | `Specificity`, `SelectorResult`, `RegexSelector`, `RegexSelectorCombinator` |
85
- | `selector.ts` | CSS セレクタエンジン | `Selector` クラス(エントリーポイントからは再エクスポートされない)、`Ruleset`, `StructuredSelector`, `SelectorTarget`(内部) |
84
+ | `types.ts` | 型定義 | `Specificity`, `SelectorResult`, `RegexSelector`, `RegexSelectorCombinator`, `SelectorAttr`, `SelectorNode`, `SelectorElement` |
85
+ | `selector.ts` | CSS セレクタエンジン | `Selector` クラス、`ExtendedPseudoClass` 型、`Ruleset`, `StructuredSelector`, `SelectorTarget`(内部) |
86
86
  | `create-selector.ts` | キャッシュ付きファクトリ | `createSelector()` |
87
87
  | `match-selector.ts` | 統合マッチング | `matchSelector()`, `SelectorMatches` |
88
88
  | `compare-specificity.ts` | 詳細度比較 | `compareSpecificity()` |
89
89
  | `regex-selector-matches.ts` | Regex マッチングヘルパー | `regexSelectorMatches()` |
90
- | `is.ts` | DOM 型ガード | `isElement()`, `isNonDocumentTypeChildNode()`, `isPureHTMLElement()` |
90
+ | `is.ts` | ノード型ガード | `isElement()`, `isNonDocumentTypeChildNode()`, `isPureHTMLElement()` |
91
91
  | `invalid-selector-error.ts` | エラークラス | `InvalidSelectorError` |
92
92
  | `debug.ts` | デバッグログ | `log`(debug インスタンス), `enableDebug()` |
93
93
  | `aria-pseudo-class.ts` | `:aria()` ハンドラ | `ariaPseudoClass()` |
@@ -102,7 +102,7 @@ flowchart TD
102
102
 
103
103
  ### `matchSelector(el, selector, scope?, specs?)`
104
104
 
105
- CSS セレクタ文字列または `RegexSelector` オブジェクトの両方を受け付ける統合マッチング関数です。`{ matched: true, selector, specificity, data? }` または `{ matched: false }` を返します。
105
+ CSS セレクタ文字列または `RegexSelector` オブジェクトの両方を受け付ける統合マッチング関数です。`el` および `scope` パラメータは任意の `SelectorNode` を受け取ります。`{ matched: true, selector, specificity, data? }` または `{ matched: false }` を返します。
106
106
 
107
107
  ### `compareSpecificity(a, b)`
108
108
 
@@ -116,6 +116,22 @@ CSS セレクタ文字列または `RegexSelector` オブジェクトの両方
116
116
 
117
117
  CSS セレクタ文字列がパースできない場合にスローされるカスタムエラーです。
118
118
 
119
+ ## 純粋データインターフェース
120
+
121
+ セレクタエンジンは DOM `Element`/`Node` を直接使用せず、純粋なデータインターフェース上で動作します。これにより、マッチングロジックが約200プロパティの DOM API から分離され、移植性(例: Rust/WASM)とプレーンオブジェクトによるテストが可能になります。
122
+
123
+ | インターフェース | 継承元 | 用途 |
124
+ | ----------------- | -------------- | ------------------------------------------------------ |
125
+ | `SelectorAttr` | — | 最小属性: `name`, `localName`, `value`, `namespaceURI` |
126
+ | `SelectorNode` | — | 最小ノード: `nodeType`, `nodeName`, `parentNode` |
127
+ | `SelectorElement` | `SelectorNode` | エンジンが実際に読み取る約12のプロパティを持つ要素 |
128
+
129
+ DOM `Element`、JSDOM 要素、`MLElement` はすべて構造的型付けにより `SelectorElement` を満たします — アダプタコードは不要です。
130
+
131
+ 拡張擬似クラス(`:aria()`、`:role()`、`:model()`)は `SelectorElement` を受け取り、完全な DOM API が必要な `@markuplint/ml-spec` 境界で `Element` にキャストします。
132
+
133
+ `@markuplint/selector` は依存グラフ上 `@markuplint/ml-core` より下位に位置するため、`MLElement` を直接参照できません。`:aria()` 擬似クラスはダックタイピング(`'getAccessibleName' in el`)により、要素がキャッシュ付きアクセシブルネームメソッドを持つかを検出します。これにより、循環依存を導入することなく `MLElement` の要素単位メモ化キャッシュをセレクタから共有できます。
134
+
119
135
  ## コア内部クラス
120
136
 
121
137
  CSS マッチングシステムは 4 つのクラスの階層で構成されます:
@@ -136,7 +152,7 @@ Selector
136
152
 
137
153
  ### CSS セレクタマッチング
138
154
 
139
- 標準 CSS セレクタは `postcss-selector-parser` により AST にパースされ、DOM ノードに対してマッチングされます:
155
+ 標準 CSS セレクタは `postcss-selector-parser` により AST にパースされ、`SelectorNode`/`SelectorElement` インスタンスに対してマッチングされます:
140
156
 
141
157
  1. `createSelector()` がキャッシュされた `Selector` インスタンスを作成または取得
142
158
  2. `Selector.match()` が `Ruleset.match()` に委譲
@@ -161,16 +177,16 @@ Regex セレクタは `RegexSelector` 型を使用してパターンで要素を
161
177
  拡張擬似クラスは `ExtendedPseudoClass` 型を通じて登録されます:
162
178
 
163
179
  ```typescript
164
- type ExtendedPseudoClass = Record<string, (content: string) => (el: Element) => SelectorResult>;
180
+ type ExtendedPseudoClass = Record<string, (content: string) => (el: SelectorElement) => SelectorResult>;
165
181
  ```
166
182
 
167
183
  3 つの擬似クラスが組み込まれています:
168
184
 
169
- | 擬似クラス | モジュール | 説明 |
170
- | ---------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------- |
171
- | `:aria(has name)` / `:aria(has no name)` | `aria-pseudo-class.ts` | `getAccname()` を使用してアクセシブルネームの有無で要素をマッチング |
172
- | `:role(roleName)` / `:role(roleName\|version)` | `aria-role-pseudo-class.ts` | `getComputedRole()` を使用して計算された ARIA ロールで要素をマッチング |
173
- | `:model(category)` | `content-model-pseudo-class.ts` | HTML コンテンツモデルカテゴリに属する要素をマッチング |
185
+ | 擬似クラス | モジュール | 説明 |
186
+ | ---------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
187
+ | `:aria(has name)` / `:aria(has no name)` | `aria-pseudo-class.ts` | アクセシブルネームの有無で要素をマッチング(`MLElement.getAccessibleName()` のキャッシュを利用、フォールバックとして `getAccname()` を使用) |
188
+ | `:role(roleName)` / `:role(roleName\|version)` | `aria-role-pseudo-class.ts` | `getComputedRole()` を使用して計算された ARIA ロールで要素をマッチング |
189
+ | `:model(category)` | `content-model-pseudo-class.ts` | HTML コンテンツモデルカテゴリに属する要素をマッチング |
174
190
 
175
191
  すべての拡張擬似クラスの詳細度は `[0, 1, 0]` です。
176
192
 
package/ARCHITECTURE.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `@markuplint/selector` is an extended [W3C Selectors Level 4](https://www.w3.org/TR/selectors-4/) matcher for markuplint. It provides two independent matching systems:
6
6
 
7
- 1. **CSS Selector Matching** -- Parses standard CSS selectors via `postcss-selector-parser` and matches them against DOM nodes with full specificity tracking.
7
+ 1. **CSS Selector Matching** -- Parses standard CSS selectors via `postcss-selector-parser` and matches them against elements with full specificity tracking.
8
8
  2. **Regex Selector Matching** -- Matches elements using regular expression patterns on node names and attributes, with captured group data extraction.
9
9
 
10
10
  The package also defines markuplint-specific extended pseudo-classes (`:aria()`, `:role()`, `:model()`) that integrate HTML/ARIA specification data into selector matching.
@@ -14,13 +14,13 @@ The package also defines markuplint-specific extended pseudo-classes (`:aria()`,
14
14
  ```
15
15
  src/
16
16
  ├── index.ts — Export entry point
17
- ├── types.ts — Type definitions (Specificity, SelectorResult, RegexSelector, etc.)
17
+ ├── types.ts — Type definitions (SelectorElement, SelectorNode, SelectorAttr, Specificity, SelectorResult, etc.)
18
18
  ├── selector.ts — Core Selector/Ruleset/StructuredSelector/SelectorTarget classes
19
19
  ├── create-selector.ts — Selector factory with instance caching and extended pseudo-class registration
20
20
  ├── match-selector.ts — CSS/Regex selector matching public function
21
21
  ├── compare-specificity.ts — Specificity comparison utility
22
22
  ├── regex-selector-matches.ts — Regex pattern matching helper
23
- ├── is.ts — DOM node type guards
23
+ ├── is.ts — Node type guards (SelectorNode/SelectorElement)
24
24
  ├── invalid-selector-error.ts — Custom error class for invalid selectors
25
25
  ├── debug.ts — Debug log configuration (using debug package)
26
26
  └── extended-selector/
@@ -78,21 +78,21 @@ flowchart TD
78
78
 
79
79
  ## Module Overview
80
80
 
81
- | Module | Role | Key Exports |
82
- | ------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
83
- | `index.ts` | Entry point | Re-exports all public API |
84
- | `types.ts` | Type definitions | `Specificity`, `SelectorResult`, `RegexSelector`, `RegexSelectorCombinator` |
85
- | `selector.ts` | CSS selector engine | `Selector` class (not re-exported from entry point), `Ruleset`, `StructuredSelector`, `SelectorTarget` (internal) |
86
- | `create-selector.ts` | Factory with caching | `createSelector()` |
87
- | `match-selector.ts` | Unified matching | `matchSelector()`, `SelectorMatches` |
88
- | `compare-specificity.ts` | Specificity comparison | `compareSpecificity()` |
89
- | `regex-selector-matches.ts` | Regex matching helper | `regexSelectorMatches()` |
90
- | `is.ts` | DOM type guards | `isElement()`, `isNonDocumentTypeChildNode()`, `isPureHTMLElement()` |
91
- | `invalid-selector-error.ts` | Error class | `InvalidSelectorError` |
92
- | `debug.ts` | Debug logging | `log` (debug instance), `enableDebug()` |
93
- | `aria-pseudo-class.ts` | `:aria()` handler | `ariaPseudoClass()` |
94
- | `aria-role-pseudo-class.ts` | `:role()` handler | `ariaRolePseudoClass()` |
95
- | `content-model-pseudo-class.ts` | `:model()` handler | `contentModelPseudoClass()` |
81
+ | Module | Role | Key Exports |
82
+ | ------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
83
+ | `index.ts` | Entry point | Re-exports all public API |
84
+ | `types.ts` | Type definitions | `Specificity`, `SelectorResult`, `RegexSelector`, `RegexSelectorCombinator`, `SelectorAttr`, `SelectorNode`, `SelectorElement` |
85
+ | `selector.ts` | CSS selector engine | `Selector` class, `ExtendedPseudoClass` type, `Ruleset`, `StructuredSelector`, `SelectorTarget` (internal) |
86
+ | `create-selector.ts` | Factory with caching | `createSelector()` |
87
+ | `match-selector.ts` | Unified matching | `matchSelector()`, `SelectorMatches` |
88
+ | `compare-specificity.ts` | Specificity comparison | `compareSpecificity()` |
89
+ | `regex-selector-matches.ts` | Regex matching helper | `regexSelectorMatches()` |
90
+ | `is.ts` | Node type guards | `isElement()`, `isNonDocumentTypeChildNode()`, `isPureHTMLElement()` |
91
+ | `invalid-selector-error.ts` | Error class | `InvalidSelectorError` |
92
+ | `debug.ts` | Debug logging | `log` (debug instance), `enableDebug()` |
93
+ | `aria-pseudo-class.ts` | `:aria()` handler | `ariaPseudoClass()` |
94
+ | `aria-role-pseudo-class.ts` | `:role()` handler | `ariaRolePseudoClass()` |
95
+ | `content-model-pseudo-class.ts` | `:model()` handler | `contentModelPseudoClass()` |
96
96
 
97
97
  ## Public API
98
98
 
@@ -102,7 +102,7 @@ Creates a cached `Selector` instance. When `specs` is provided, the extended pse
102
102
 
103
103
  ### `matchSelector(el, selector, scope?, specs?)`
104
104
 
105
- Unified matching function that accepts either a CSS selector string or a `RegexSelector` object. Returns `{ matched: true, selector, specificity, data? }` or `{ matched: false }`.
105
+ Unified matching function that accepts either a CSS selector string or a `RegexSelector` object. The `el` and `scope` parameters accept any `SelectorNode`. Returns `{ matched: true, selector, specificity, data? }` or `{ matched: false }`.
106
106
 
107
107
  ### `compareSpecificity(a, b)`
108
108
 
@@ -116,6 +116,22 @@ Union type for selector match results: `{ matched: true, selector, specificity,
116
116
 
117
117
  Custom error thrown when a CSS selector string cannot be parsed.
118
118
 
119
+ ## Pure Data Interfaces
120
+
121
+ The selector engine operates on pure data interfaces rather than DOM `Element`/`Node` directly. This decouples the matching logic from the ~200-property DOM API, making it portable (e.g., to Rust/WASM) and testable with plain objects.
122
+
123
+ | Interface | Extends | Purpose |
124
+ | ----------------- | -------------- | --------------------------------------------------------------- |
125
+ | `SelectorAttr` | — | Minimal attribute: `name`, `localName`, `value`, `namespaceURI` |
126
+ | `SelectorNode` | — | Minimal node: `nodeType`, `nodeName`, `parentNode` |
127
+ | `SelectorElement` | `SelectorNode` | Element with the ~12 properties the engine actually reads |
128
+
129
+ DOM `Element`, JSDOM elements, and `MLElement` all satisfy `SelectorElement` via structural typing — no adapter code is needed.
130
+
131
+ Extended pseudo-classes (`:aria()`, `:role()`, `:model()`) receive a `SelectorElement` and cast to `Element` at the `@markuplint/ml-spec` boundary where full DOM APIs are required.
132
+
133
+ Because `@markuplint/selector` sits below `@markuplint/ml-core` in the dependency graph, it cannot reference `MLElement` directly. The `:aria()` pseudo-class uses duck-typing (`'getAccessibleName' in el`) to detect whether the element provides a cached accessible name method. This allows the selector to share `MLElement`'s per-element memoization cache without introducing a circular dependency.
134
+
119
135
  ## Core Internal Classes
120
136
 
121
137
  The CSS matching system uses a hierarchy of four classes:
@@ -136,7 +152,7 @@ Selector
136
152
 
137
153
  ### CSS Selector Matching
138
154
 
139
- Standard CSS selectors are parsed by `postcss-selector-parser` into an AST, then matched against DOM nodes. The matching process:
155
+ Standard CSS selectors are parsed by `postcss-selector-parser` into an AST, then matched against `SelectorNode`/`SelectorElement` instances. The matching process:
140
156
 
141
157
  1. `createSelector()` creates or retrieves a cached `Selector` instance
142
158
  2. `Selector.match()` delegates to `Ruleset.match()`
@@ -161,16 +177,16 @@ See [Selector Matching](docs/matching.md) for detailed algorithm documentation.
161
177
  Extended pseudo-classes are registered through the `ExtendedPseudoClass` type:
162
178
 
163
179
  ```typescript
164
- type ExtendedPseudoClass = Record<string, (content: string) => (el: Element) => SelectorResult>;
180
+ type ExtendedPseudoClass = Record<string, (content: string) => (el: SelectorElement) => SelectorResult>;
165
181
  ```
166
182
 
167
183
  Three pseudo-classes are built in:
168
184
 
169
- | Pseudo-Class | Module | Description |
170
- | ---------------------------------------------- | ------------------------------- | ----------------------------------------------------------------- |
171
- | `:aria(has name)` / `:aria(has no name)` | `aria-pseudo-class.ts` | Matches elements by accessible name presence using `getAccname()` |
172
- | `:role(roleName)` / `:role(roleName\|version)` | `aria-role-pseudo-class.ts` | Matches elements by computed ARIA role using `getComputedRole()` |
173
- | `:model(category)` | `content-model-pseudo-class.ts` | Matches elements belonging to an HTML content model category |
185
+ | Pseudo-Class | Module | Description |
186
+ | ---------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
187
+ | `:aria(has name)` / `:aria(has no name)` | `aria-pseudo-class.ts` | Matches elements by accessible name presence (uses `MLElement.getAccessibleName()` cache when available, falls back to `getAccname()`) |
188
+ | `:role(roleName)` / `:role(roleName\|version)` | `aria-role-pseudo-class.ts` | Matches elements by computed ARIA role using `getComputedRole()` |
189
+ | `:model(category)` | `content-model-pseudo-class.ts` | Matches elements belonging to an HTML content model category |
174
190
 
175
191
  All extended pseudo-classes have a specificity of `[0, 1, 0]`.
176
192
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
7
+
8
+ ### Bug Fixes
9
+
10
+ - resolve additional eslint-plugin-unicorn v63 errors ([e58a72c](https://github.com/markuplint/markuplint/commit/e58a72c17c97bbec522f9513b99777fac6904d64))
11
+ - use explicit `export type` for type-only re-exports ([7c77c05](https://github.com/markuplint/markuplint/commit/7c77c05619518c8d18a183132040f5b2cd0ab6ec))
12
+
13
+ ### Performance Improvements
14
+
15
+ - **selector:** use cached getAccessibleName in :aria() pseudo-class ([42ea466](https://github.com/markuplint/markuplint/commit/42ea4665308e2b90e90856b29be939a6e8022347)), closes [#2179](https://github.com/markuplint/markuplint/issues/2179)
16
+
6
17
  ## [4.7.8](https://github.com/markuplint/markuplint/compare/@markuplint/selector@4.7.7...@markuplint/selector@4.7.8) (2026-02-10)
7
18
 
8
19
  **Note:** Version bump only for package @markuplint/selector
package/SKILL.md CHANGED
@@ -40,7 +40,7 @@ Add a new markuplint-specific extended pseudo-class. Follow recipe #1 in `docs/m
40
40
  2. Create `src/extended-selector/<name>-pseudo-class.ts`
41
41
  3. Implement the handler following the `ExtendedPseudoClass` signature:
42
42
  ```typescript
43
- (content: string) => (el: Element) => SelectorResult;
43
+ (content: string) => (el: SelectorElement) => SelectorResult;
44
44
  ```
45
45
  4. Return specificity `[0, 1, 0]` for consistency with other extended pseudo-classes
46
46
 
@@ -41,12 +41,14 @@ yarn workspace @markuplint/selector run vitest run src/selector.spec.ts
41
41
  1. `src/extended-selector/custom-pseudo-class.ts` を作成:
42
42
 
43
43
  ```typescript
44
- import type { SelectorResult } from '../types.js';
44
+ import type { SelectorElement, SelectorResult } from '../types.js';
45
45
 
46
46
  export function customPseudoClass() {
47
47
  return (content: string) =>
48
- (el: Element): SelectorResult => {
48
+ (el: SelectorElement): SelectorResult => {
49
49
  // content 文字列をパースして el に対してマッチング
50
+ // 完全な DOM API(例: @markuplint/ml-spec)が必要な場合は、
51
+ // その境界で `el as Element` とキャストしてください。
50
52
  const matched = /* マッチングロジック */;
51
53
  return {
52
54
  specificity: [0, 1, 0],
@@ -206,4 +208,4 @@ Regex セレクタに新しいコンビネータを追加するには:
206
208
  ### jsdom(開発)
207
209
 
208
210
  - テストで DOM 環境を作成するための `JSDOM` を提供
209
- - `jsdom` で作成された要素はセレクタマッチングロジックが使用する標準 DOM API を持つ
211
+ - `jsdom` で作成された要素はセレクタマッチングロジックが使用する `SelectorElement` インターフェースを満たす
@@ -41,12 +41,14 @@ To add a new markuplint-specific pseudo-class (e.g., `:custom()`):
41
41
  1. Create `src/extended-selector/custom-pseudo-class.ts`:
42
42
 
43
43
  ```typescript
44
- import type { SelectorResult } from '../types.js';
44
+ import type { SelectorElement, SelectorResult } from '../types.js';
45
45
 
46
46
  export function customPseudoClass() {
47
47
  return (content: string) =>
48
- (el: Element): SelectorResult => {
48
+ (el: SelectorElement): SelectorResult => {
49
49
  // Parse content string and match against el
50
+ // If you need full DOM APIs (e.g., from @markuplint/ml-spec),
51
+ // cast with `el as Element` at that boundary.
50
52
  const matched = /* your matching logic */;
51
53
  return {
52
54
  specificity: [0, 1, 0],
@@ -206,4 +208,4 @@ To add a new combinator for regex selectors:
206
208
  ### jsdom (dev)
207
209
 
208
210
  - Provides `JSDOM` for creating DOM environments in tests
209
- - Elements created via `jsdom` have standard DOM APIs used by the selector matching logic
211
+ - Elements created via `jsdom` satisfy the `SelectorElement` interface used by the selector matching logic
@@ -9,7 +9,7 @@
9
9
  1. **CSS セレクタマッチング** -- `postcss-selector-parser` でパースされる標準 CSS セレクタ
10
10
  2. **Regex セレクタマッチング** -- 正規表現を使用したパターンベースのマッチング
11
11
 
12
- 両システムとも詳細度情報を返し、統合関数 `matchSelector()` を通じて使用できます。
12
+ 両システムとも DOM `Element` ではなく純粋データインターフェース(`SelectorNode`/`SelectorElement`)上で動作し、詳細度情報を返し、統合関数 `matchSelector()` を通じて使用できます。
13
13
 
14
14
  ## CSS セレクタマッチングフロー
15
15
 
@@ -83,7 +83,7 @@ div > .class:not(.other) span
83
83
 
84
84
  ### 拡張擬似クラス
85
85
 
86
- 拡張擬似クラスは `ExtendedPseudoClass` レジストリを通じてディスパッチされます:
86
+ 拡張擬似クラスは `ExtendedPseudoClass` レジストリを通じてディスパッチされます。ハンドラは `SelectorElement` を受け取り、`@markuplint/ml-spec` API を呼び出す際に `Element` にキャストします:
87
87
 
88
88
  #### `:aria(syntax)`
89
89
 
package/docs/matching.md CHANGED
@@ -9,7 +9,7 @@ The package provides two independent matching systems:
9
9
  1. **CSS Selector Matching** -- Standard CSS selectors parsed via `postcss-selector-parser`
10
10
  2. **Regex Selector Matching** -- Pattern-based matching using regular expressions
11
11
 
12
- Both systems return specificity information and can be used through the unified `matchSelector()` function.
12
+ Both systems operate on pure data interfaces (`SelectorNode`/`SelectorElement`) rather than DOM `Element` directly, return specificity information, and can be used through the unified `matchSelector()` function.
13
13
 
14
14
  ## CSS Selector Matching Flow
15
15
 
@@ -83,7 +83,7 @@ Walks up the ancestor chain and matches if any ancestor matches the inner select
83
83
 
84
84
  ### Extended Pseudo-Classes
85
85
 
86
- Extended pseudo-classes are dispatched through the `ExtendedPseudoClass` registry:
86
+ Extended pseudo-classes are dispatched through the `ExtendedPseudoClass` registry. Handlers receive a `SelectorElement` and cast to `Element` when calling `@markuplint/ml-spec` APIs:
87
87
 
88
88
  #### `:aria(syntax)`
89
89
 
@@ -7,4 +7,4 @@ import type { Specificity } from './types.js';
7
7
  * @param b - The second specificity tuple `[id, class, type]`
8
8
  * @returns `-1` if `a` is less specific, `1` if `a` is more specific, `0` if equal
9
9
  */
10
- export declare function compareSpecificity(a: Specificity, b: Specificity): 1 | -1 | 0;
10
+ export declare function compareSpecificity(a: Specificity, b: Specificity): 0 | 1 | -1;
@@ -24,7 +24,7 @@ export function createSelector(selector, specs) {
24
24
  instance = new Selector(selector, specs
25
25
  ? {
26
26
  model: contentModelPseudoClass(specs),
27
- aria: ariaPseudoClass(),
27
+ aria: ariaPseudoClass(specs),
28
28
  role: ariaRolePseudoClass(specs),
29
29
  }
30
30
  : undefined);
@@ -1,11 +1,28 @@
1
- import type { SelectorResult } from '../types.js';
1
+ import type { SelectorElement, SelectorResult } from '../types.js';
2
+ import type { MLMLSpec } from '@markuplint/ml-spec';
2
3
  /**
3
4
  * Creates the `:aria()` extended pseudo-class handler.
4
5
  *
5
- * Matches elements by accessible name presence using `getAccname()`.
6
+ * Matches elements by accessible name presence.
6
7
  * Supports `has name` and `has no name` syntax.
7
8
  * Version syntax is parsed but not yet used for filtering.
8
9
  *
10
+ * ## Accessible name resolution strategy
11
+ *
12
+ * When the element exposes a `getAccessibleName()` method (i.e. it is an
13
+ * `MLElement` from `@markuplint/ml-core`), that method is called via
14
+ * duck-typing so that its per-element memoization cache is shared with
15
+ * other consumers (`require-accessible-name`, `wai-aria`, etc.).
16
+ *
17
+ * The `@markuplint/selector` package cannot depend on `@markuplint/ml-core`
18
+ * (it sits lower in the dependency graph), so a structural type check
19
+ * (`'getAccessibleName' in el`) is used instead of an `instanceof` guard.
20
+ *
21
+ * For elements that do **not** have the method (e.g. plain DOM `Element`
22
+ * instances in unit tests), the function falls back to the stateless
23
+ * `getAccname()` from `@markuplint/ml-spec`.
24
+ *
25
+ * @param specs - The ML specification data for role resolution
9
26
  * @returns An extended pseudo-class handler function
10
27
  */
11
- export declare function ariaPseudoClass(): (content: string) => (el: Element) => SelectorResult;
28
+ export declare function ariaPseudoClass(specs: MLMLSpec): (content: string) => (el: SelectorElement) => SelectorResult;
@@ -2,18 +2,41 @@ import { validateAriaVersion, ARIA_RECOMMENDED_VERSION, getAccname } from '@mark
2
2
  /**
3
3
  * Creates the `:aria()` extended pseudo-class handler.
4
4
  *
5
- * Matches elements by accessible name presence using `getAccname()`.
5
+ * Matches elements by accessible name presence.
6
6
  * Supports `has name` and `has no name` syntax.
7
7
  * Version syntax is parsed but not yet used for filtering.
8
8
  *
9
+ * ## Accessible name resolution strategy
10
+ *
11
+ * When the element exposes a `getAccessibleName()` method (i.e. it is an
12
+ * `MLElement` from `@markuplint/ml-core`), that method is called via
13
+ * duck-typing so that its per-element memoization cache is shared with
14
+ * other consumers (`require-accessible-name`, `wai-aria`, etc.).
15
+ *
16
+ * The `@markuplint/selector` package cannot depend on `@markuplint/ml-core`
17
+ * (it sits lower in the dependency graph), so a structural type check
18
+ * (`'getAccessibleName' in el`) is used instead of an `instanceof` guard.
19
+ *
20
+ * For elements that do **not** have the method (e.g. plain DOM `Element`
21
+ * instances in unit tests), the function falls back to the stateless
22
+ * `getAccname()` from `@markuplint/ml-spec`.
23
+ *
24
+ * @param specs - The ML specification data for role resolution
9
25
  * @returns An extended pseudo-class handler function
10
26
  */
11
- export function ariaPseudoClass() {
27
+ export function ariaPseudoClass(specs) {
12
28
  return (content) => (
13
29
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
14
30
  el) => {
15
31
  const aria = ariaPseudoClassParser(content);
16
- const name = getAccname(el);
32
+ const version = aria.version ?? ARIA_RECOMMENDED_VERSION;
33
+ // Prefer MLElement.getAccessibleName() (cached) over ml-spec's
34
+ // stateless getAccname(). The duck-typing check is necessary because
35
+ // SelectorElement is an interface that does not declare
36
+ // getAccessibleName — the concrete MLElement adds it at runtime.
37
+ const name = 'getAccessibleName' in el && typeof el.getAccessibleName === 'function'
38
+ ? el.getAccessibleName(version)
39
+ : getAccname(el, specs, version);
17
40
  switch (aria.type) {
18
41
  case 'hasName': {
19
42
  if (name) {
@@ -48,7 +71,7 @@ export function ariaPseudoClass() {
48
71
  }
49
72
  function ariaPseudoClassParser(syntax) {
50
73
  const [_query, _version] = syntax.split('|');
51
- const query = _query?.replace(/\s+/g, '').toLowerCase();
74
+ const query = _query?.replaceAll(/\s+/g, '').toLowerCase();
52
75
  const version = _version ?? ARIA_RECOMMENDED_VERSION;
53
76
  if (!validateAriaVersion(version)) {
54
77
  throw new SyntaxError(`Unsupported ARIA version: ${version}`);
@@ -1,4 +1,4 @@
1
- import type { SelectorResult } from '../types.js';
1
+ import type { SelectorElement, SelectorResult } from '../types.js';
2
2
  import type { MLMLSpec } from '@markuplint/ml-spec';
3
3
  /**
4
4
  * Creates the `:role()` extended pseudo-class handler.
@@ -9,4 +9,4 @@ import type { MLMLSpec } from '@markuplint/ml-spec';
9
9
  * @param specs - The HTML/ARIA specification data used for role computation
10
10
  * @returns An extended pseudo-class handler function
11
11
  */
12
- export declare function ariaRolePseudoClass(specs: MLMLSpec): (content: string) => (el: Element) => SelectorResult;
12
+ export declare function ariaRolePseudoClass(specs: MLMLSpec): (content: string) => (el: SelectorElement) => SelectorResult;
@@ -1,4 +1,4 @@
1
- import type { SelectorResult } from '../types.js';
1
+ import type { SelectorElement, SelectorResult } from '../types.js';
2
2
  import type { MLMLSpec } from '@markuplint/ml-spec';
3
3
  /**
4
4
  * Creates the `:model()` extended pseudo-class handler.
@@ -9,4 +9,4 @@ import type { MLMLSpec } from '@markuplint/ml-spec';
9
9
  * @param specs - The HTML/ARIA specification data containing content model definitions
10
10
  * @returns An extended pseudo-class handler function
11
11
  */
12
- export declare function contentModelPseudoClass(specs: MLMLSpec): (category: string) => (el: Element) => SelectorResult;
12
+ export declare function contentModelPseudoClass(specs: MLMLSpec): (category: string) => (el: SelectorElement) => SelectorResult;
package/lib/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { compareSpecificity } from './compare-specificity.js';
2
- export { matchSelector, SelectorMatches } from './match-selector.js';
2
+ export { matchSelector } from './match-selector.js';
3
+ export type { SelectorMatches } from './match-selector.js';
3
4
  export { createSelector } from './create-selector.js';
5
+ export { Selector } from './selector.js';
6
+ export type { ExtendedPseudoClass } from './selector.js';
4
7
  export { InvalidSelectorError } from './invalid-selector-error.js';
5
- export * from './types.js';
8
+ export type * from './types.js';
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { compareSpecificity } from './compare-specificity.js';
2
2
  export { matchSelector } from './match-selector.js';
3
3
  export { createSelector } from './create-selector.js';
4
+ export { Selector } from './selector.js';
4
5
  export { InvalidSelectorError } from './invalid-selector-error.js';
5
- export * from './types.js';
@@ -2,13 +2,15 @@
2
2
  * Error thrown when a CSS selector string cannot be parsed.
3
3
  */
4
4
  export class InvalidSelectorError extends Error {
5
+ name = 'InvalidSelectorError';
6
+ /** The invalid selector string that caused this error */
7
+ selector;
5
8
  /**
6
9
  * @param selector - The invalid selector string
7
10
  * @param message - An optional custom error message
8
11
  */
9
12
  constructor(selector, message) {
10
13
  super(message ?? `Invalid selector: "${selector}"`);
11
- this.name = 'InvalidSelectorError';
12
14
  this.selector = selector;
13
15
  }
14
16
  }
package/lib/is.d.ts CHANGED
@@ -1,25 +1,26 @@
1
+ import type { SelectorElement, SelectorNode } from './types.js';
1
2
  /**
2
3
  * Checks whether the given node is an Element node.
3
4
  *
4
- * @param node - The DOM node to check
5
+ * @param node - The node to check
5
6
  * @returns `true` if the node is an Element
6
7
  */
7
- export declare function isElement(node: Node): node is Element;
8
+ export declare function isElement(node: SelectorNode): node is SelectorElement;
8
9
  /**
9
10
  * Checks whether the given node is a non-DocumentType child node
10
11
  * (i.e., has `previousElementSibling` and `nextElementSibling` properties).
11
12
  *
12
- * @param node - The DOM node to check
13
+ * @param node - The node to check
13
14
  * @returns `true` if the node is an Element or CharacterData
14
15
  */
15
- export declare function isNonDocumentTypeChildNode(node: Node): node is Element | CharacterData;
16
+ export declare function isNonDocumentTypeChildNode(node: SelectorNode): node is SelectorElement;
16
17
  /**
17
18
  * Checks if the given element is a pure HTML element.
18
19
  *
19
20
  * If a pure HTML element, `localName` returns lowercase,
20
21
  * `nodeName` returns uppercase.
21
22
  *
22
- * @param el The element to check.
23
- * @returns Returns true if the element is a pure HTML element, otherwise returns false.
23
+ * @param el - The element to check
24
+ * @returns `true` if the element is a pure HTML element, `false` otherwise
24
25
  */
25
- export declare function isPureHTMLElement(el: Element): boolean;
26
+ export declare function isPureHTMLElement(el: SelectorElement): boolean;
package/lib/is.js CHANGED
@@ -1,24 +1,21 @@
1
+ const ELEMENT_NODE = 1;
1
2
  /**
2
3
  * Checks whether the given node is an Element node.
3
4
  *
4
- * @param node - The DOM node to check
5
+ * @param node - The node to check
5
6
  * @returns `true` if the node is an Element
6
7
  */
7
- export function isElement(
8
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
9
- node) {
10
- return node.nodeType === node.ELEMENT_NODE;
8
+ export function isElement(node) {
9
+ return node.nodeType === ELEMENT_NODE;
11
10
  }
12
11
  /**
13
12
  * Checks whether the given node is a non-DocumentType child node
14
13
  * (i.e., has `previousElementSibling` and `nextElementSibling` properties).
15
14
  *
16
- * @param node - The DOM node to check
15
+ * @param node - The node to check
17
16
  * @returns `true` if the node is an Element or CharacterData
18
17
  */
19
- export function isNonDocumentTypeChildNode(
20
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
21
- node) {
18
+ export function isNonDocumentTypeChildNode(node) {
22
19
  return 'previousElementSibling' in node && 'nextElementSibling' in node;
23
20
  }
24
21
  /**
@@ -27,8 +24,8 @@ node) {
27
24
  * If a pure HTML element, `localName` returns lowercase,
28
25
  * `nodeName` returns uppercase.
29
26
  *
30
- * @param el The element to check.
31
- * @returns Returns true if the element is a pure HTML element, otherwise returns false.
27
+ * @param el - The element to check
28
+ * @returns `true` if the element is a pure HTML element, `false` otherwise
32
29
  */
33
30
  export function isPureHTMLElement(
34
31
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1,4 +1,4 @@
1
- import type { Specificity, RegexSelector } from './types.js';
1
+ import type { SelectorNode, Specificity, RegexSelector } from './types.js';
2
2
  import type { MLMLSpec } from '@markuplint/ml-spec';
3
3
  /**
4
4
  * The result of matching a selector against a node.
@@ -15,16 +15,16 @@ type SelectorUnmatched = {
15
15
  readonly matched: false;
16
16
  };
17
17
  /**
18
- * Matches a CSS selector or regex selector against a DOM node.
18
+ * Matches a CSS selector or regex selector against a node.
19
19
  *
20
20
  * Supports both standard CSS selectors (as strings) and markuplint's
21
21
  * {@link RegexSelector} for pattern-based matching with captured groups.
22
22
  *
23
- * @param el - The DOM node to test
23
+ * @param el - The node to test
24
24
  * @param selector - A CSS selector string, a regex selector object, or `undefined`
25
25
  * @param scope - The scope element for `:scope` pseudo-class resolution
26
26
  * @param specs - The HTML/ARIA specification data for extended pseudo-classes
27
27
  * @returns A match result with specificity and captured data, or `{ matched: false }`
28
28
  */
29
- export declare function matchSelector(el: Node, selector: string | RegexSelector | undefined, scope?: ParentNode | null, specs?: MLMLSpec): SelectorMatches;
29
+ export declare function matchSelector(el: SelectorNode, selector: string | RegexSelector | undefined, scope?: SelectorNode | null, specs?: MLMLSpec): SelectorMatches;
30
30
  export {};
@@ -1,35 +1,19 @@
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 _SelectorTarget_combinedFrom, _SelectorTarget_selector;
13
1
  import { isElement, isNonDocumentTypeChildNode, isPureHTMLElement } from './is.js';
14
2
  import { regexSelectorMatches } from './regex-selector-matches.js';
15
3
  import { createSelector } from './create-selector.js';
16
4
  /**
17
- * Matches a CSS selector or regex selector against a DOM node.
5
+ * Matches a CSS selector or regex selector against a node.
18
6
  *
19
7
  * Supports both standard CSS selectors (as strings) and markuplint's
20
8
  * {@link RegexSelector} for pattern-based matching with captured groups.
21
9
  *
22
- * @param el - The DOM node to test
10
+ * @param el - The node to test
23
11
  * @param selector - A CSS selector string, a regex selector object, or `undefined`
24
12
  * @param scope - The scope element for `:scope` pseudo-class resolution
25
13
  * @param specs - The HTML/ARIA specification data for extended pseudo-classes
26
14
  * @returns A match result with specificity and captured data, or `{ matched: false }`
27
15
  */
28
- export function matchSelector(
29
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
30
- el, selector,
31
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
32
- scope, specs) {
16
+ export function matchSelector(el, selector, scope, specs) {
33
17
  if (selector == null || selector === '') {
34
18
  return {
35
19
  matched: false,
@@ -51,9 +35,7 @@ scope, specs) {
51
35
  }
52
36
  return regexSelect(el, selector);
53
37
  }
54
- function regexSelect(
55
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
56
- el, selector) {
38
+ function regexSelect(el, selector) {
57
39
  let edge = new SelectorTarget(selector);
58
40
  let edgeSelector = selector.combination;
59
41
  while (edgeSelector) {
@@ -65,28 +47,26 @@ el, selector) {
65
47
  return edge.match(el);
66
48
  }
67
49
  class SelectorTarget {
50
+ #combinedFrom = null;
51
+ #selector;
68
52
  constructor(selector) {
69
- _SelectorTarget_combinedFrom.set(this, null);
70
- _SelectorTarget_selector.set(this, void 0);
71
- __classPrivateFieldSet(this, _SelectorTarget_selector, selector, "f");
53
+ this.#selector = selector;
72
54
  }
73
55
  from(target, combinator) {
74
- __classPrivateFieldSet(this, _SelectorTarget_combinedFrom, { target, combinator }, "f");
56
+ this.#combinedFrom = { target, combinator };
75
57
  }
76
- match(
77
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
78
- el) {
58
+ match(el) {
79
59
  const unitCheck = this._matchWithoutCombineChecking(el);
80
60
  if (!unitCheck.matched) {
81
61
  return unitCheck;
82
62
  }
83
- if (!__classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f")) {
63
+ if (!this.#combinedFrom) {
84
64
  return unitCheck;
85
65
  }
86
66
  if (!isNonDocumentTypeChildNode(el)) {
87
67
  return unitCheck;
88
68
  }
89
- const { target, combinator } = __classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f");
69
+ const { target, combinator } = this.#combinedFrom;
90
70
  switch (combinator) {
91
71
  // Descendant combinator
92
72
  case ' ': {
@@ -161,20 +141,15 @@ class SelectorTarget {
161
141
  return { matched: false };
162
142
  }
163
143
  default: {
164
- throw new Error(`Unsupported ${__classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f").combinator} combinator in selector`);
144
+ throw new Error(`Unsupported ${this.#combinedFrom.combinator} combinator in selector`);
165
145
  }
166
146
  }
167
147
  }
168
- _matchWithoutCombineChecking(
169
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
170
- el) {
171
- return uncombinedRegexSelect(el, __classPrivateFieldGet(this, _SelectorTarget_selector, "f"));
148
+ _matchWithoutCombineChecking(el) {
149
+ return uncombinedRegexSelect(el, this.#selector);
172
150
  }
173
151
  }
174
- _SelectorTarget_combinedFrom = new WeakMap(), _SelectorTarget_selector = new WeakMap();
175
- function uncombinedRegexSelect(
176
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
177
- el, selector) {
152
+ function uncombinedRegexSelect(el, selector) {
178
153
  if (!isElement(el)) {
179
154
  return {
180
155
  matched: false,
@@ -239,13 +214,13 @@ el, selector) {
239
214
  specificity[1] += specifiedAttr.size;
240
215
  if (matched) {
241
216
  return {
242
- matched,
217
+ matched: true,
243
218
  selector: `${tagSelector}${attrSelector}`,
244
219
  specificity,
245
220
  data,
246
221
  };
247
222
  }
248
- return { matched };
223
+ return { matched: false };
249
224
  }
250
225
  function mergeMatches(a, b, sep, close = false) {
251
226
  return {
package/lib/selector.d.ts CHANGED
@@ -1,14 +1,39 @@
1
- import type { SelectorResult, Specificity } from './types.js';
2
- type ExtendedPseudoClass = Readonly<Record<string, (content: string) => (el: Element) => SelectorResult>>;
1
+ import type { SelectorElement, SelectorNode, SelectorResult, Specificity } from './types.js';
3
2
  /**
4
- * CSS selector matcher that parses a selector string and matches it against DOM nodes.
3
+ * Registry of extended pseudo-class handlers keyed by pseudo-class name.
4
+ *
5
+ * Each handler is a curried function: given the pseudo-class content string,
6
+ * it returns a matcher that tests a {@link SelectorElement} and produces
7
+ * a {@link SelectorResult}.
8
+ */
9
+ export type ExtendedPseudoClass = Readonly<Record<string, (content: string) => (el: SelectorElement) => SelectorResult>>;
10
+ /**
11
+ * CSS selector matcher that parses a selector string and matches it against nodes.
5
12
  *
6
13
  * Use {@link createSelector} to create cached instances with extended pseudo-class support.
7
14
  */
8
15
  export declare class Selector {
9
16
  #private;
17
+ /**
18
+ * @param selector - The CSS selector string to parse
19
+ * @param extended - Extended pseudo-class handlers to register
20
+ */
10
21
  constructor(selector: string, extended?: ExtendedPseudoClass);
11
- match(el: Node, scope?: ParentNode | null): Specificity | false;
12
- search(el: Node, scope?: ParentNode | null): SelectorResult[];
22
+ /**
23
+ * Tests whether the given node matches this selector.
24
+ *
25
+ * @param el - The node to test
26
+ * @param scope - The scope node for `:scope` pseudo-class resolution
27
+ * @returns The specificity of the first matching selector, or `false` if none matched
28
+ */
29
+ match(el: SelectorNode, scope?: SelectorNode | null): Specificity | false;
30
+ /**
31
+ * Evaluates all comma-separated selectors against the given node
32
+ * and returns each result (matched or unmatched).
33
+ *
34
+ * @param el - The node to test
35
+ * @param scope - The scope node for `:scope` pseudo-class resolution
36
+ * @returns An array of results, one per comma-separated selector alternative
37
+ */
38
+ search(el: SelectorNode, scope?: SelectorNode | null): SelectorResult[];
13
39
  }
14
- export {};
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';
@@ -19,20 +7,27 @@ import { isElement, isNonDocumentTypeChildNode, isPureHTMLElement } from './is.j
19
7
  const selLog = coreLog.extend('selector');
20
8
  const resLog = coreLog.extend('result');
21
9
  /**
22
- * CSS selector matcher that parses a selector string and matches it against DOM nodes.
10
+ * CSS selector matcher that parses a selector string and matches it against nodes.
23
11
  *
24
12
  * Use {@link createSelector} to create cached instances with extended pseudo-class support.
25
13
  */
26
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
+ */
27
20
  constructor(selector, extended = {}) {
28
- _Selector_ruleset.set(this, void 0);
29
- __classPrivateFieldSet(this, _Selector_ruleset, Ruleset.parse(selector, extended), "f");
21
+ this.#ruleset = Ruleset.parse(selector, extended);
30
22
  }
31
- match(
32
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
33
- el,
34
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
35
- 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) {
36
31
  scope = scope ?? (isElement(el) ? el : null);
37
32
  const results = this.search(el, scope);
38
33
  for (const result of results) {
@@ -42,16 +37,19 @@ export class Selector {
42
37
  }
43
38
  return false;
44
39
  }
45
- search(
46
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
47
- el,
48
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
49
- 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) {
50
49
  scope = scope ?? (isElement(el) ? el : null);
51
- return __classPrivateFieldGet(this, _Selector_ruleset, "f").match(el, scope);
50
+ return this.#ruleset.match(el, scope);
52
51
  }
53
52
  }
54
- _Selector_ruleset = new WeakMap();
55
53
  class Ruleset {
56
54
  static parse(selector, extended) {
57
55
  const selectors = [];
@@ -68,27 +66,24 @@ class Ruleset {
68
66
  }
69
67
  return new Ruleset(selectors, extended, 0);
70
68
  }
69
+ headCombinator;
70
+ #selectorGroup = [];
71
71
  constructor(selectors, extended, depth) {
72
- _Ruleset_selectorGroup.set(this, []);
73
- __classPrivateFieldGet(this, _Ruleset_selectorGroup, "f").push(...selectors.map(selector => new StructuredSelector(selector, depth, extended)));
74
- 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];
75
74
  this.headCombinator = head?.headCombinator ?? null;
76
75
  if (this.headCombinator && depth <= 0) {
77
- if (__classPrivateFieldGet(this, _Ruleset_selectorGroup, "f")[0]?.selector) {
78
- throw new InvalidSelectorError(__classPrivateFieldGet(this, _Ruleset_selectorGroup, "f")[0]?.selector);
76
+ if (this.#selectorGroup[0]?.selector) {
77
+ throw new InvalidSelectorError(this.#selectorGroup[0]?.selector);
79
78
  }
80
79
  throw new Error('Combinated selector depth is not expected');
81
80
  }
82
81
  }
83
- match(
84
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
85
- el,
86
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
87
- scope) {
82
+ match(el, scope) {
88
83
  if (coreLog.enabled) {
89
84
  coreLog('<%s> (%s)', isElement(el) ? el.localName : el.nodeName, scope ? (isElement(scope) ? scope.localName : scope.nodeName) : null);
90
85
  }
91
- return __classPrivateFieldGet(this, _Ruleset_selectorGroup, "f").map(selector => {
86
+ return this.#selectorGroup.map(selector => {
92
87
  if (selLog.enabled) {
93
88
  selLog('"%s"', selector.selector);
94
89
  }
@@ -100,16 +95,16 @@ class Ruleset {
100
95
  });
101
96
  }
102
97
  }
103
- _Ruleset_selectorGroup = new WeakMap();
104
98
  class StructuredSelector {
99
+ #edge;
100
+ headCombinator;
101
+ #selector;
105
102
  constructor(selector, depth, extended) {
106
- _StructuredSelector_edge.set(this, void 0);
107
- _StructuredSelector_selector.set(this, void 0);
108
- __classPrivateFieldSet(this, _StructuredSelector_selector, selector, "f");
109
- __classPrivateFieldSet(this, _StructuredSelector_edge, new SelectorTarget(extended, depth), "f");
103
+ this.#selector = selector;
104
+ this.#edge = new SelectorTarget(extended, depth);
110
105
  this.headCombinator =
111
- __classPrivateFieldGet(this, _StructuredSelector_selector, "f").nodes[0]?.type === 'combinator' ? (__classPrivateFieldGet(this, _StructuredSelector_selector, "f").nodes[0].value ?? null) : null;
112
- 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];
113
108
  if (0 < depth && this.headCombinator) {
114
109
  nodes.unshift(parser.pseudo({ value: ':scope' }));
115
110
  }
@@ -117,8 +112,8 @@ class StructuredSelector {
117
112
  switch (node.type) {
118
113
  case 'combinator': {
119
114
  const combinedTarget = new SelectorTarget(extended, depth);
120
- combinedTarget.from(__classPrivateFieldGet(this, _StructuredSelector_edge, "f"), node);
121
- __classPrivateFieldSet(this, _StructuredSelector_edge, combinedTarget, "f");
115
+ combinedTarget.from(this.#edge, node);
116
+ this.#edge = combinedTarget;
122
117
  break;
123
118
  }
124
119
  case 'root':
@@ -132,38 +127,34 @@ class StructuredSelector {
132
127
  throw new Error(`Unsupported comment in selector: ${selector.toString()}`);
133
128
  }
134
129
  default: {
135
- __classPrivateFieldGet(this, _StructuredSelector_edge, "f").add(node);
130
+ this.#edge.add(node);
136
131
  }
137
132
  }
138
133
  }
139
134
  }
140
135
  get selector() {
141
- return __classPrivateFieldGet(this, _StructuredSelector_selector, "f").nodes.join('');
136
+ return this.#selector.nodes.join('');
142
137
  }
143
- match(
144
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
145
- el,
146
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
147
- scope) {
148
- return __classPrivateFieldGet(this, _StructuredSelector_edge, "f").match(el, scope, 0);
138
+ match(el, scope) {
139
+ return this.#edge.match(el, scope, 0);
149
140
  }
150
141
  }
151
- _StructuredSelector_edge = new WeakMap(), _StructuredSelector_selector = new WeakMap();
152
142
  class SelectorTarget {
143
+ attr = [];
144
+ class = [];
145
+ #combinedFrom = null;
146
+ depth;
147
+ #extended;
148
+ id = [];
149
+ #isAdded = false;
150
+ pseudo = [];
151
+ tag = null;
153
152
  constructor(extended, depth) {
154
- this.attr = [];
155
- this.class = [];
156
- _SelectorTarget_combinedFrom.set(this, null);
157
- _SelectorTarget_extended.set(this, void 0);
158
- this.id = [];
159
- _SelectorTarget_isAdded.set(this, false);
160
- this.pseudo = [];
161
- this.tag = null;
162
- __classPrivateFieldSet(this, _SelectorTarget_extended, extended, "f");
153
+ this.#extended = extended;
163
154
  this.depth = depth;
164
155
  }
165
156
  add(selector) {
166
- __classPrivateFieldSet(this, _SelectorTarget_isAdded, true, "f");
157
+ this.#isAdded = true;
167
158
  switch (selector.type) {
168
159
  case 'tag':
169
160
  case 'universal': {
@@ -189,17 +180,13 @@ class SelectorTarget {
189
180
  }
190
181
  }
191
182
  from(target, combinator) {
192
- __classPrivateFieldSet(this, _SelectorTarget_combinedFrom, { target, combinator }, "f");
183
+ this.#combinedFrom = { target, combinator };
193
184
  }
194
- match(
195
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
196
- el,
197
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
198
- scope, count) {
185
+ match(el, scope, count) {
199
186
  const result = this._match(el, scope, count);
200
187
  if (selLog.enabled) {
201
188
  const nodeName = el.nodeName;
202
- const selector = __classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f")?.target.toString() ?? this.toString();
189
+ const selector = this.#combinedFrom?.target.toString() ?? this.toString();
203
190
  const combinator = result.combinator ? ` ${result.combinator}` : '';
204
191
  selLog('The %s element by "%s" => %s (%d)', nodeName, `${selector}${combinator}`, result.matched, count);
205
192
  if (selector === ':scope') {
@@ -218,22 +205,18 @@ class SelectorTarget {
218
205
  this.pseudo.map(pseudo => pseudo.value).join(''),
219
206
  ].join('');
220
207
  }
221
- _match(
222
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
223
- el,
224
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
225
- scope, count) {
208
+ _match(el, scope, count) {
226
209
  const unitCheck = this._matchWithoutCombineChecking(el, scope);
227
210
  if (!unitCheck.matched) {
228
211
  return unitCheck;
229
212
  }
230
- if (!__classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f")) {
213
+ if (!this.#combinedFrom) {
231
214
  return unitCheck;
232
215
  }
233
216
  if (!isNonDocumentTypeChildNode(el)) {
234
217
  return unitCheck;
235
218
  }
236
- const { target, combinator } = __classPrivateFieldGet(this, _SelectorTarget_combinedFrom, "f");
219
+ const { target, combinator } = this.#combinedFrom;
237
220
  switch (combinator.value) {
238
221
  // Descendant combinator
239
222
  case ' ': {
@@ -429,7 +412,7 @@ class SelectorTarget {
429
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).');
430
413
  }
431
414
  default: {
432
- 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`);
433
416
  }
434
417
  }
435
418
  }
@@ -441,11 +424,7 @@ class SelectorTarget {
441
424
  * @param scope
442
425
  * @private
443
426
  */
444
- _matchWithoutCombineChecking(
445
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
446
- el,
447
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
448
- scope) {
427
+ _matchWithoutCombineChecking(el, scope) {
449
428
  const specificity = [0, 0, 0];
450
429
  if (!isElement(el)) {
451
430
  return {
@@ -477,7 +456,7 @@ class SelectorTarget {
477
456
  }
478
457
  }
479
458
  let matched = true;
480
- if (!__classPrivateFieldGet(this, _SelectorTarget_isAdded, "f") && !isScope(el, scope)) {
459
+ if (!this.#isAdded && !isScope(el, scope)) {
481
460
  matched = false;
482
461
  }
483
462
  if (matched && !this.id.every(id => id.value === el.id)) {
@@ -506,7 +485,7 @@ class SelectorTarget {
506
485
  specificity[1] += this.attr.length;
507
486
  if (matched) {
508
487
  for (const pseudo of this.pseudo) {
509
- const pseudoRes = pseudoMatch(pseudo, el, scope, __classPrivateFieldGet(this, _SelectorTarget_extended, "f"), this.depth);
488
+ const pseudoRes = pseudoMatch(pseudo, el, scope, this.#extended, this.depth);
510
489
  specificity[0] += pseudoRes.specificity[0];
511
490
  specificity[1] += pseudoRes.specificity[1];
512
491
  specificity[2] += pseudoRes.specificity[2];
@@ -522,19 +501,18 @@ class SelectorTarget {
522
501
  if (matched) {
523
502
  return {
524
503
  specificity,
525
- matched,
504
+ matched: true,
526
505
  nodes: [el],
527
506
  has,
528
507
  };
529
508
  }
530
509
  return {
531
510
  specificity,
532
- matched,
511
+ matched: false,
533
512
  not,
534
513
  };
535
514
  }
536
515
  }
537
- _SelectorTarget_combinedFrom = new WeakMap(), _SelectorTarget_extended = new WeakMap(), _SelectorTarget_isAdded = new WeakMap();
538
516
  function attrMatch(attr,
539
517
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
540
518
  el) {
@@ -599,9 +577,7 @@ el) {
599
577
  }
600
578
  function pseudoMatch(pseudo,
601
579
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
602
- el,
603
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
604
- scope, extended, depth) {
580
+ el, scope, extended, depth) {
605
581
  switch (pseudo.value) {
606
582
  //
607
583
  /**
@@ -808,9 +784,7 @@ scope, extended, depth) {
808
784
  }
809
785
  function isScope(
810
786
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
811
- el,
812
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
813
- scope) {
787
+ el, scope) {
814
788
  return el === scope || el.parentNode === null;
815
789
  }
816
790
  function getDescendants(
package/lib/types.d.ts CHANGED
@@ -1,3 +1,49 @@
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
+ }
1
47
  /**
2
48
  * A CSS specificity tuple: `[id, class, type]`.
3
49
  * Each component counts selectors of the corresponding category.
@@ -15,8 +61,8 @@ export type SelectorMatchedResult = {
15
61
  /** The computed specificity of the matched selector */
16
62
  readonly specificity: Specificity;
17
63
  readonly matched: true;
18
- /** The DOM nodes that were matched */
19
- readonly nodes: readonly (Element | Text)[];
64
+ /** The elements that were matched */
65
+ readonly nodes: readonly SelectorElement[];
20
66
  /** Results from `:has()` pseudo-class sub-matches */
21
67
  readonly has: readonly SelectorMatchedResult[];
22
68
  };
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@markuplint/selector",
3
- "version": "4.7.8",
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.2",
30
+ "@markuplint/ml-spec": "5.0.0-alpha.0",
28
31
  "@types/debug": "4.1.12",
29
32
  "debug": "4.4.3",
30
33
  "postcss-selector-parser": "7.1.1",
31
- "type-fest": "4.41.0"
34
+ "type-fest": "5.4.4"
32
35
  },
33
36
  "devDependencies": {
34
37
  "@types/jsdom": "27.0.0",
35
- "jsdom": "26.1.0"
38
+ "jsdom": "28.1.0"
36
39
  },
37
- "gitHead": "193ee7c1262bbed95424e38efdf1a8e56ff049f4"
40
+ "gitHead": "13dcfc84ec83d87360c720e253383b60767e1b56"
38
41
  }