@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/ARCHITECTURE.ja.md +259 -0
- package/ARCHITECTURE.md +259 -0
- package/CHANGELOG.md +13 -2
- package/README.md +6 -0
- package/SKILL.md +126 -0
- package/docs/maintenance.ja.md +211 -0
- package/docs/maintenance.md +211 -0
- package/docs/matching.ja.md +211 -0
- package/docs/matching.md +211 -0
- package/lib/compare-specificity.d.ts +9 -1
- package/lib/compare-specificity.js +8 -0
- package/lib/create-selector.d.ts +13 -0
- package/lib/create-selector.js +14 -1
- package/lib/debug.d.ts +5 -0
- package/lib/debug.js +5 -0
- package/lib/extended-selector/aria-pseudo-class.d.ts +26 -3
- package/lib/extended-selector/aria-pseudo-class.js +33 -4
- package/lib/extended-selector/aria-role-pseudo-class.d.ts +11 -2
- package/lib/extended-selector/aria-role-pseudo-class.js +9 -0
- package/lib/extended-selector/content-model-pseudo-class.d.ts +11 -2
- package/lib/extended-selector/content-model-pseudo-class.js +9 -0
- package/lib/index.d.ts +5 -2
- package/lib/index.js +1 -1
- package/lib/invalid-selector-error.d.ts +8 -0
- package/lib/invalid-selector-error.js +10 -1
- package/lib/is.d.ts +19 -5
- package/lib/is.js +19 -9
- package/lib/match-selector.d.ts +18 -2
- package/lib/match-selector.js +27 -40
- package/lib/regex-selector-matches.d.ts +12 -0
- package/lib/regex-selector-matches.js +12 -0
- package/lib/selector.d.ts +35 -5
- package/lib/selector.js +75 -96
- package/lib/types.d.ts +89 -1
- package/package.json +10 -7
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# @markuplint/selector
|
|
2
|
+
|
|
3
|
+
## 概要
|
|
4
|
+
|
|
5
|
+
`@markuplint/selector` は markuplint のための拡張 [W3C Selectors Level 4](https://www.w3.org/TR/selectors-4/) マッチャーです。2 つの独立したマッチングシステムを提供します:
|
|
6
|
+
|
|
7
|
+
1. **CSS セレクタマッチング** -- `postcss-selector-parser` を使用して標準 CSS セレクタをパースし、要素に対して完全な詳細度追跡付きでマッチングします。
|
|
8
|
+
2. **Regex セレクタマッチング** -- ノード名と属性に対する正規表現パターンでマッチングし、キャプチャグループデータを抽出します。
|
|
9
|
+
|
|
10
|
+
また、markuplint 固有の拡張擬似クラス(`:aria()`、`:role()`、`:model()`)を定義し、HTML/ARIA 仕様データをセレクタマッチングに統合します。
|
|
11
|
+
|
|
12
|
+
## ディレクトリ構成
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/
|
|
16
|
+
├── index.ts — エクスポートエントリーポイント
|
|
17
|
+
├── types.ts — 型定義(SelectorElement, SelectorNode, SelectorAttr, Specificity, SelectorResult 等)
|
|
18
|
+
├── selector.ts — コア Selector/Ruleset/StructuredSelector/SelectorTarget クラス
|
|
19
|
+
├── create-selector.ts — インスタンスキャッシュと拡張擬似クラス登録を持つ Selector ファクトリ
|
|
20
|
+
├── match-selector.ts — CSS/Regex セレクタマッチング公開関数
|
|
21
|
+
├── compare-specificity.ts — 詳細度比較ユーティリティ
|
|
22
|
+
├── regex-selector-matches.ts — 正規表現パターンマッチングヘルパー
|
|
23
|
+
├── is.ts — ノード型ガード(SelectorNode/SelectorElement)
|
|
24
|
+
├── invalid-selector-error.ts — 無効セレクタ用カスタムエラークラス
|
|
25
|
+
├── debug.ts — デバッグログ設定(debug パッケージ使用)
|
|
26
|
+
└── extended-selector/
|
|
27
|
+
├── aria-pseudo-class.ts — :aria() 擬似クラス(アクセシブルネーム判定)
|
|
28
|
+
├── aria-role-pseudo-class.ts — :role() 擬似クラス(計算された ARIA ロール判定)
|
|
29
|
+
└── content-model-pseudo-class.ts — :model() 擬似クラス(HTML コンテンツモデルカテゴリ判定)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## アーキテクチャ図
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart TD
|
|
36
|
+
subgraph publicAPI ["公開 API"]
|
|
37
|
+
createSelector["createSelector()"]
|
|
38
|
+
matchSelector["matchSelector()"]
|
|
39
|
+
compareSpec["compareSpecificity()"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
subgraph cssMatching ["CSS セレクタマッチング"]
|
|
43
|
+
Selector["Selector"]
|
|
44
|
+
Ruleset["Ruleset"]
|
|
45
|
+
StructuredSelector["StructuredSelector"]
|
|
46
|
+
SelectorTarget["SelectorTarget\n(CSS)"]
|
|
47
|
+
psp["postcss-selector-parser"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
subgraph regexMatching ["Regex セレクタマッチング"]
|
|
51
|
+
RegexSelectorTarget["SelectorTarget\n(Regex)"]
|
|
52
|
+
regexSelectorMatches["regexSelectorMatches()"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
subgraph extendedPseudo ["拡張擬似クラス"]
|
|
56
|
+
aria[":aria()"]
|
|
57
|
+
role[":role()"]
|
|
58
|
+
model[":model()"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
subgraph deps ["外部依存"]
|
|
62
|
+
mlSpec["@markuplint/ml-spec"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
createSelector -->|"生成&キャッシュ"| Selector
|
|
66
|
+
Selector -->|"パース委譲"| Ruleset
|
|
67
|
+
Ruleset -->|"パーサ呼出"| psp
|
|
68
|
+
Ruleset -->|"保持"| StructuredSelector
|
|
69
|
+
StructuredSelector -->|"チェーン"| SelectorTarget
|
|
70
|
+
|
|
71
|
+
matchSelector -->|"CSS 文字列"| createSelector
|
|
72
|
+
matchSelector -->|"RegexSelector"| RegexSelectorTarget
|
|
73
|
+
RegexSelectorTarget -->|"使用"| regexSelectorMatches
|
|
74
|
+
|
|
75
|
+
SelectorTarget -->|"ディスパッチ"| extendedPseudo
|
|
76
|
+
mlSpec -->|"仕様データ"| extendedPseudo
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## モジュール概要
|
|
80
|
+
|
|
81
|
+
| モジュール | 役割 | 主要エクスポート |
|
|
82
|
+
| ------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
83
|
+
| `index.ts` | エントリーポイント | 全公開 API の再エクスポート |
|
|
84
|
+
| `types.ts` | 型定義 | `Specificity`, `SelectorResult`, `RegexSelector`, `RegexSelectorCombinator`, `SelectorAttr`, `SelectorNode`, `SelectorElement` |
|
|
85
|
+
| `selector.ts` | CSS セレクタエンジン | `Selector` クラス、`ExtendedPseudoClass` 型、`Ruleset`, `StructuredSelector`, `SelectorTarget`(内部) |
|
|
86
|
+
| `create-selector.ts` | キャッシュ付きファクトリ | `createSelector()` |
|
|
87
|
+
| `match-selector.ts` | 統合マッチング | `matchSelector()`, `SelectorMatches` |
|
|
88
|
+
| `compare-specificity.ts` | 詳細度比較 | `compareSpecificity()` |
|
|
89
|
+
| `regex-selector-matches.ts` | Regex マッチングヘルパー | `regexSelectorMatches()` |
|
|
90
|
+
| `is.ts` | ノード型ガード | `isElement()`, `isNonDocumentTypeChildNode()`, `isPureHTMLElement()` |
|
|
91
|
+
| `invalid-selector-error.ts` | エラークラス | `InvalidSelectorError` |
|
|
92
|
+
| `debug.ts` | デバッグログ | `log`(debug インスタンス), `enableDebug()` |
|
|
93
|
+
| `aria-pseudo-class.ts` | `:aria()` ハンドラ | `ariaPseudoClass()` |
|
|
94
|
+
| `aria-role-pseudo-class.ts` | `:role()` ハンドラ | `ariaRolePseudoClass()` |
|
|
95
|
+
| `content-model-pseudo-class.ts` | `:model()` ハンドラ | `contentModelPseudoClass()` |
|
|
96
|
+
|
|
97
|
+
## 公開 API
|
|
98
|
+
|
|
99
|
+
### `createSelector(selector, specs?)`
|
|
100
|
+
|
|
101
|
+
キャッシュされた `Selector` インスタンスを作成します。`specs` を指定すると、拡張擬似クラス(`:model()`、`:aria()`、`:role()`)が利用可能になります。インスタンスはセレクタ文字列をキーとしてキャッシュされます。
|
|
102
|
+
|
|
103
|
+
### `matchSelector(el, selector, scope?, specs?)`
|
|
104
|
+
|
|
105
|
+
CSS セレクタ文字列または `RegexSelector` オブジェクトの両方を受け付ける統合マッチング関数です。`el` および `scope` パラメータは任意の `SelectorNode` を受け取ります。`{ matched: true, selector, specificity, data? }` または `{ matched: false }` を返します。
|
|
106
|
+
|
|
107
|
+
### `compareSpecificity(a, b)`
|
|
108
|
+
|
|
109
|
+
2 つの `Specificity` タプル(`[id, class, type]`)を比較します。`-1`、`0`、`1` を返します。
|
|
110
|
+
|
|
111
|
+
### `SelectorMatches`(型)
|
|
112
|
+
|
|
113
|
+
セレクタマッチ結果のユニオン型: `{ matched: true, selector, specificity, data? } | { matched: false }`。
|
|
114
|
+
|
|
115
|
+
### `InvalidSelectorError`
|
|
116
|
+
|
|
117
|
+
CSS セレクタ文字列がパースできない場合にスローされるカスタムエラーです。
|
|
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
|
+
|
|
135
|
+
## コア内部クラス
|
|
136
|
+
|
|
137
|
+
CSS マッチングシステムは 4 つのクラスの階層で構成されます:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Selector
|
|
141
|
+
└── Ruleset(postcss-selector-parser でパース)
|
|
142
|
+
└── StructuredSelector[](カンマ区切りセレクタごとに 1 つ)
|
|
143
|
+
└── SelectorTarget[](コンビネータで連結されたチェーン)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **Selector** -- 公開クラス。マッチングと検索を `Ruleset` に委譲します。
|
|
147
|
+
- **Ruleset** -- `postcss-selector-parser` でセレクタ文字列をパースし、`StructuredSelector` インスタンスのグループを保持します。カンマ区切りの全選択肢に対する `SelectorResult` 配列を返します。
|
|
148
|
+
- **StructuredSelector** -- 単一のセレクタ(カンマなし)を表現します。コンビネータで連結された `SelectorTarget` ノードのチェーンを構築します。マッチング時は右から左へチェーンをたどります。
|
|
149
|
+
- **SelectorTarget** -- 単一の複合セレクタを要素に対してマッチングします。ID、タグ、クラス、属性、ユニバーサルセレクタ、擬似クラス(拡張擬似クラスを含む)を処理します。
|
|
150
|
+
|
|
151
|
+
## 2 つのマッチングシステム
|
|
152
|
+
|
|
153
|
+
### CSS セレクタマッチング
|
|
154
|
+
|
|
155
|
+
標準 CSS セレクタは `postcss-selector-parser` により AST にパースされ、`SelectorNode`/`SelectorElement` インスタンスに対してマッチングされます:
|
|
156
|
+
|
|
157
|
+
1. `createSelector()` がキャッシュされた `Selector` インスタンスを作成または取得
|
|
158
|
+
2. `Selector.match()` が `Ruleset.match()` に委譲
|
|
159
|
+
3. 各 `StructuredSelector` が AST から `SelectorTarget` チェーンを構築
|
|
160
|
+
4. `SelectorTarget` が個別の複合セレクタ(ID、クラス、タグ、属性、擬似クラス)をマッチング
|
|
161
|
+
5. コンビネータ(子孫、子、兄弟)が `SelectorTarget` ノード間の DOM をトラバース
|
|
162
|
+
|
|
163
|
+
### Regex セレクタマッチング
|
|
164
|
+
|
|
165
|
+
Regex セレクタは `RegexSelector` 型を使用してパターンで要素をマッチングします:
|
|
166
|
+
|
|
167
|
+
1. `matchSelector()` が `RegexSelector` オブジェクトを受け取る
|
|
168
|
+
2. `combination` リンクから `SelectorTarget` チェーンが構築される
|
|
169
|
+
3. 各ターゲットが `regexSelectorMatches()` を使用して `nodeName`、`attrName`、`attrValue` をマッチング
|
|
170
|
+
4. マッチしたキャプチャグループが `data` レコード(`$0`、`$1`、名前付きグループ)に収集される
|
|
171
|
+
5. コンビネータは標準 CSS コンビネータに加え、前方兄弟マッチング用の `:has(+)` と `:has(~)` を含む
|
|
172
|
+
|
|
173
|
+
詳細なアルゴリズムは[セレクタマッチング](docs/matching.ja.md)を参照してください。
|
|
174
|
+
|
|
175
|
+
## 拡張擬似クラスシステム
|
|
176
|
+
|
|
177
|
+
拡張擬似クラスは `ExtendedPseudoClass` 型を通じて登録されます:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
type ExtendedPseudoClass = Record<string, (content: string) => (el: SelectorElement) => SelectorResult>;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
3 つの擬似クラスが組み込まれています:
|
|
184
|
+
|
|
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 コンテンツモデルカテゴリに属する要素をマッチング |
|
|
190
|
+
|
|
191
|
+
すべての拡張擬似クラスの詳細度は `[0, 1, 0]` です。
|
|
192
|
+
|
|
193
|
+
## 詳細度
|
|
194
|
+
|
|
195
|
+
詳細度はマッチング全体を通じて `[id, class, type]` タプルとして追跡されます。主要なルール:
|
|
196
|
+
|
|
197
|
+
- `:where()` は `[0, 0, 0]` の詳細度
|
|
198
|
+
- `:not()`、`:is()`、`:has()` は最も詳細度の高い引数の詳細度
|
|
199
|
+
- 拡張擬似クラスは `[0, 1, 0]`
|
|
200
|
+
- `compareSpecificity()` は左から右に比較(ID > class > type)
|
|
201
|
+
|
|
202
|
+
## 外部依存パッケージ
|
|
203
|
+
|
|
204
|
+
| パッケージ | 用途 |
|
|
205
|
+
| ------------------------- | --------------------------------------------------------- |
|
|
206
|
+
| `postcss-selector-parser` | CSS セレクタ文字列の AST へのパース |
|
|
207
|
+
| `@markuplint/ml-spec` | 拡張擬似クラス用の HTML/ARIA 仕様データ |
|
|
208
|
+
| `debug` | `DEBUG=selector*` によるデバッグログ |
|
|
209
|
+
| `type-fest` | TypeScript ユーティリティ型(`ReadonlyDeep`, `Writable`) |
|
|
210
|
+
|
|
211
|
+
開発依存:
|
|
212
|
+
|
|
213
|
+
| パッケージ | 用途 |
|
|
214
|
+
| ---------- | ----------------- |
|
|
215
|
+
| `jsdom` | テスト用 DOM 環境 |
|
|
216
|
+
|
|
217
|
+
## 他パッケージとの連携
|
|
218
|
+
|
|
219
|
+
```mermaid
|
|
220
|
+
flowchart LR
|
|
221
|
+
subgraph upstream ["上流パッケージ"]
|
|
222
|
+
mlSpec["@markuplint/ml-spec\n(仕様データ + 型)"]
|
|
223
|
+
psp["postcss-selector-parser\n(CSS パーサ)"]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
subgraph pkg ["@markuplint/selector"]
|
|
227
|
+
createSel["createSelector()"]
|
|
228
|
+
matchSel["matchSelector()"]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
subgraph downstream ["下流パッケージ"]
|
|
232
|
+
mlCore["@markuplint/ml-core"]
|
|
233
|
+
rules["@markuplint/rules"]
|
|
234
|
+
mlConfig["@markuplint/ml-config"]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
mlSpec -->|"仕様データ\n(ARIA, コンテンツモデル)"| pkg
|
|
238
|
+
psp -->|"セレクタ AST"| pkg
|
|
239
|
+
|
|
240
|
+
matchSel -->|"matchSelector()"| mlCore
|
|
241
|
+
createSel -->|"createSelector().search()"| rules
|
|
242
|
+
pkg -->|"RegexSelector 型"| mlConfig
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 上流
|
|
246
|
+
|
|
247
|
+
- **`@markuplint/ml-spec`** は拡張擬似クラスで使用される ARIA 仕様データ(`getComputedRole`、`getAccname`、`contentModelCategoryToTagNames`)と `resolveNamespace()` ユーティリティを提供します。
|
|
248
|
+
- **`postcss-selector-parser`** は CSS セレクタ文字列を `Ruleset` が使用する AST にパースします。
|
|
249
|
+
|
|
250
|
+
### 下流
|
|
251
|
+
|
|
252
|
+
- **`@markuplint/ml-core`** はリント実行中にルール設定を要素にマッチングするために `matchSelector()` を使用します。
|
|
253
|
+
- **`@markuplint/rules`** はルール実装での要素選択に `createSelector().search()` を使用します。
|
|
254
|
+
- **`@markuplint/ml-config`** は設定スキーマ定義に `RegexSelector` 型を使用します。
|
|
255
|
+
|
|
256
|
+
## ドキュメントマップ
|
|
257
|
+
|
|
258
|
+
- [セレクタマッチング](docs/matching.ja.md) -- CSS と Regex セレクタマッチングアルゴリズムの詳細
|
|
259
|
+
- [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、テスト、レシピ、トラブルシューティング
|
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# @markuplint/selector
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
1. **CSS Selector Matching** -- Parses standard CSS selectors via `postcss-selector-parser` and matches them against elements with full specificity tracking.
|
|
8
|
+
2. **Regex Selector Matching** -- Matches elements using regular expression patterns on node names and attributes, with captured group data extraction.
|
|
9
|
+
|
|
10
|
+
The package also defines markuplint-specific extended pseudo-classes (`:aria()`, `:role()`, `:model()`) that integrate HTML/ARIA specification data into selector matching.
|
|
11
|
+
|
|
12
|
+
## Directory Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/
|
|
16
|
+
├── index.ts — Export entry point
|
|
17
|
+
├── types.ts — Type definitions (SelectorElement, SelectorNode, SelectorAttr, Specificity, SelectorResult, etc.)
|
|
18
|
+
├── selector.ts — Core Selector/Ruleset/StructuredSelector/SelectorTarget classes
|
|
19
|
+
├── create-selector.ts — Selector factory with instance caching and extended pseudo-class registration
|
|
20
|
+
├── match-selector.ts — CSS/Regex selector matching public function
|
|
21
|
+
├── compare-specificity.ts — Specificity comparison utility
|
|
22
|
+
├── regex-selector-matches.ts — Regex pattern matching helper
|
|
23
|
+
├── is.ts — Node type guards (SelectorNode/SelectorElement)
|
|
24
|
+
├── invalid-selector-error.ts — Custom error class for invalid selectors
|
|
25
|
+
├── debug.ts — Debug log configuration (using debug package)
|
|
26
|
+
└── extended-selector/
|
|
27
|
+
├── aria-pseudo-class.ts — :aria() pseudo-class (accessible name matching)
|
|
28
|
+
├── aria-role-pseudo-class.ts — :role() pseudo-class (computed ARIA role matching)
|
|
29
|
+
└── content-model-pseudo-class.ts — :model() pseudo-class (HTML content model category matching)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Architecture Diagram
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart TD
|
|
36
|
+
subgraph publicAPI ["Public API"]
|
|
37
|
+
createSelector["createSelector()"]
|
|
38
|
+
matchSelector["matchSelector()"]
|
|
39
|
+
compareSpec["compareSpecificity()"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
subgraph cssMatching ["CSS Selector Matching"]
|
|
43
|
+
Selector["Selector"]
|
|
44
|
+
Ruleset["Ruleset"]
|
|
45
|
+
StructuredSelector["StructuredSelector"]
|
|
46
|
+
SelectorTarget["SelectorTarget\n(CSS)"]
|
|
47
|
+
psp["postcss-selector-parser"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
subgraph regexMatching ["Regex Selector Matching"]
|
|
51
|
+
RegexSelectorTarget["SelectorTarget\n(Regex)"]
|
|
52
|
+
regexSelectorMatches["regexSelectorMatches()"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
subgraph extendedPseudo ["Extended Pseudo-Classes"]
|
|
56
|
+
aria[":aria()"]
|
|
57
|
+
role[":role()"]
|
|
58
|
+
model[":model()"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
subgraph deps ["External Dependencies"]
|
|
62
|
+
mlSpec["@markuplint/ml-spec"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
createSelector -->|"creates & caches"| Selector
|
|
66
|
+
Selector -->|"parses via"| Ruleset
|
|
67
|
+
Ruleset -->|"delegates to"| psp
|
|
68
|
+
Ruleset -->|"contains"| StructuredSelector
|
|
69
|
+
StructuredSelector -->|"chain of"| SelectorTarget
|
|
70
|
+
|
|
71
|
+
matchSelector -->|"CSS string"| createSelector
|
|
72
|
+
matchSelector -->|"RegexSelector"| RegexSelectorTarget
|
|
73
|
+
RegexSelectorTarget -->|"uses"| regexSelectorMatches
|
|
74
|
+
|
|
75
|
+
SelectorTarget -->|"dispatches"| extendedPseudo
|
|
76
|
+
mlSpec -->|"spec data"| extendedPseudo
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Module Overview
|
|
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`, `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
|
+
|
|
97
|
+
## Public API
|
|
98
|
+
|
|
99
|
+
### `createSelector(selector, specs?)`
|
|
100
|
+
|
|
101
|
+
Creates a cached `Selector` instance. When `specs` is provided, the extended pseudo-classes (`:model()`, `:aria()`, `:role()`) become available. Instances are cached by selector string for reuse.
|
|
102
|
+
|
|
103
|
+
### `matchSelector(el, selector, scope?, specs?)`
|
|
104
|
+
|
|
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
|
+
|
|
107
|
+
### `compareSpecificity(a, b)`
|
|
108
|
+
|
|
109
|
+
Compares two `Specificity` tuples (`[id, class, type]`). Returns `-1`, `0`, or `1`.
|
|
110
|
+
|
|
111
|
+
### `SelectorMatches` (type)
|
|
112
|
+
|
|
113
|
+
Union type for selector match results: `{ matched: true, selector, specificity, data? } | { matched: false }`.
|
|
114
|
+
|
|
115
|
+
### `InvalidSelectorError`
|
|
116
|
+
|
|
117
|
+
Custom error thrown when a CSS selector string cannot be parsed.
|
|
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
|
+
|
|
135
|
+
## Core Internal Classes
|
|
136
|
+
|
|
137
|
+
The CSS matching system uses a hierarchy of four classes:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Selector
|
|
141
|
+
└── Ruleset (parsed from postcss-selector-parser)
|
|
142
|
+
└── StructuredSelector[] (one per comma-separated selector)
|
|
143
|
+
└── SelectorTarget[] (chain linked by combinators)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **Selector** -- Public-facing class. Delegates to `Ruleset` for matching and searching.
|
|
147
|
+
- **Ruleset** -- Parses a selector string via `postcss-selector-parser` and holds a group of `StructuredSelector` instances. Returns an array of `SelectorResult` for all comma-separated alternatives.
|
|
148
|
+
- **StructuredSelector** -- Represents a single selector (without commas). Builds a chain of `SelectorTarget` nodes linked by combinators. Traverses the chain from right to left during matching.
|
|
149
|
+
- **SelectorTarget** -- Matches a single compound selector against an element. Handles ID, tag, class, attribute, universal selectors, and pseudo-classes (including extended pseudo-classes).
|
|
150
|
+
|
|
151
|
+
## Two Matching Systems
|
|
152
|
+
|
|
153
|
+
### CSS Selector Matching
|
|
154
|
+
|
|
155
|
+
Standard CSS selectors are parsed by `postcss-selector-parser` into an AST, then matched against `SelectorNode`/`SelectorElement` instances. The matching process:
|
|
156
|
+
|
|
157
|
+
1. `createSelector()` creates or retrieves a cached `Selector` instance
|
|
158
|
+
2. `Selector.match()` delegates to `Ruleset.match()`
|
|
159
|
+
3. Each `StructuredSelector` builds a `SelectorTarget` chain from the AST
|
|
160
|
+
4. `SelectorTarget` matches individual compounds (ID, class, tag, attributes, pseudo-classes)
|
|
161
|
+
5. Combinators (descendant, child, sibling) traverse the DOM between `SelectorTarget` nodes
|
|
162
|
+
|
|
163
|
+
### Regex Selector Matching
|
|
164
|
+
|
|
165
|
+
Regex selectors use the `RegexSelector` type to match elements by patterns:
|
|
166
|
+
|
|
167
|
+
1. `matchSelector()` receives a `RegexSelector` object
|
|
168
|
+
2. A `SelectorTarget` chain is built from the `combination` links
|
|
169
|
+
3. Each target matches `nodeName`, `attrName`, and/or `attrValue` using `regexSelectorMatches()`
|
|
170
|
+
4. Matched capture groups are collected into a `data` record (`$0`, `$1`, named groups)
|
|
171
|
+
5. Combinators include standard CSS combinators plus `:has(+)` and `:has(~)` for forward-sibling matching
|
|
172
|
+
|
|
173
|
+
See [Selector Matching](docs/matching.md) for detailed algorithm documentation.
|
|
174
|
+
|
|
175
|
+
## Extended Pseudo-Class System
|
|
176
|
+
|
|
177
|
+
Extended pseudo-classes are registered through the `ExtendedPseudoClass` type:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
type ExtendedPseudoClass = Record<string, (content: string) => (el: SelectorElement) => SelectorResult>;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Three pseudo-classes are built in:
|
|
184
|
+
|
|
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 |
|
|
190
|
+
|
|
191
|
+
All extended pseudo-classes have a specificity of `[0, 1, 0]`.
|
|
192
|
+
|
|
193
|
+
## Specificity
|
|
194
|
+
|
|
195
|
+
Specificity is tracked as a `[id, class, type]` tuple throughout matching. Key rules:
|
|
196
|
+
|
|
197
|
+
- `:where()` contributes `[0, 0, 0]` specificity
|
|
198
|
+
- `:not()`, `:is()`, `:has()` contribute the specificity of their most specific argument
|
|
199
|
+
- Extended pseudo-classes contribute `[0, 1, 0]`
|
|
200
|
+
- `compareSpecificity()` compares left-to-right (ID > class > type)
|
|
201
|
+
|
|
202
|
+
## External Dependencies
|
|
203
|
+
|
|
204
|
+
| Dependency | Purpose |
|
|
205
|
+
| ------------------------- | -------------------------------------------------------- |
|
|
206
|
+
| `postcss-selector-parser` | CSS selector string parsing into AST |
|
|
207
|
+
| `@markuplint/ml-spec` | HTML/ARIA specification data for extended pseudo-classes |
|
|
208
|
+
| `debug` | Debug logging via `DEBUG=selector*` |
|
|
209
|
+
| `type-fest` | TypeScript utility types (`ReadonlyDeep`, `Writable`) |
|
|
210
|
+
|
|
211
|
+
Dev dependencies:
|
|
212
|
+
|
|
213
|
+
| Dependency | Purpose |
|
|
214
|
+
| ---------- | ------------------------- |
|
|
215
|
+
| `jsdom` | DOM environment for tests |
|
|
216
|
+
|
|
217
|
+
## Integration Points
|
|
218
|
+
|
|
219
|
+
```mermaid
|
|
220
|
+
flowchart LR
|
|
221
|
+
subgraph upstream ["Upstream"]
|
|
222
|
+
mlSpec["@markuplint/ml-spec\n(spec data + types)"]
|
|
223
|
+
psp["postcss-selector-parser\n(CSS parser)"]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
subgraph pkg ["@markuplint/selector"]
|
|
227
|
+
createSel["createSelector()"]
|
|
228
|
+
matchSel["matchSelector()"]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
subgraph downstream ["Downstream"]
|
|
232
|
+
mlCore["@markuplint/ml-core"]
|
|
233
|
+
rules["@markuplint/rules"]
|
|
234
|
+
mlConfig["@markuplint/ml-config"]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
mlSpec -->|"spec data\n(ARIA, content models)"| pkg
|
|
238
|
+
psp -->|"selector AST"| pkg
|
|
239
|
+
|
|
240
|
+
matchSel -->|"matchSelector()"| mlCore
|
|
241
|
+
createSel -->|"createSelector().search()"| rules
|
|
242
|
+
pkg -->|"RegexSelector type"| mlConfig
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Upstream
|
|
246
|
+
|
|
247
|
+
- **`@markuplint/ml-spec`** provides ARIA specification data (`getComputedRole`, `getAccname`, `contentModelCategoryToTagNames`) used by extended pseudo-classes and the `resolveNamespace()` utility.
|
|
248
|
+
- **`postcss-selector-parser`** parses CSS selector strings into an AST consumed by `Ruleset`.
|
|
249
|
+
|
|
250
|
+
### Downstream
|
|
251
|
+
|
|
252
|
+
- **`@markuplint/ml-core`** uses `matchSelector()` to match rule configurations against elements during linting.
|
|
253
|
+
- **`@markuplint/rules`** uses `createSelector().search()` for element selection in rule implementations.
|
|
254
|
+
- **`@markuplint/ml-config`** uses the `RegexSelector` type for configuration schema definitions.
|
|
255
|
+
|
|
256
|
+
## Documentation Map
|
|
257
|
+
|
|
258
|
+
- [Selector Matching](docs/matching.md) -- CSS and regex selector matching algorithm details
|
|
259
|
+
- [Maintenance Guide](docs/maintenance.md) -- Commands, testing, recipes, and troubleshooting
|
package/CHANGELOG.md
CHANGED
|
@@ -3,13 +3,24 @@
|
|
|
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
|
-
|
|
6
|
+
# [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### Bug Fixes
|
|
9
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))
|
|
10
12
|
|
|
13
|
+
### Performance Improvements
|
|
11
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)
|
|
12
16
|
|
|
17
|
+
## [4.7.8](https://github.com/markuplint/markuplint/compare/@markuplint/selector@4.7.7...@markuplint/selector@4.7.8) (2026-02-10)
|
|
18
|
+
|
|
19
|
+
**Note:** Version bump only for package @markuplint/selector
|
|
20
|
+
|
|
21
|
+
## [4.7.7](https://github.com/markuplint/markuplint/compare/@markuplint/selector@4.7.6...@markuplint/selector@4.7.7) (2025-11-05)
|
|
22
|
+
|
|
23
|
+
**Note:** Version bump only for package @markuplint/selector
|
|
13
24
|
|
|
14
25
|
## [4.7.6](https://github.com/markuplint/markuplint/compare/@markuplint/selector@4.7.5...@markuplint/selector@4.7.6) (2025-08-24)
|
|
15
26
|
|
package/README.md
CHANGED
|
@@ -111,6 +111,12 @@ For example, `:role(interactive)` matches `<a>`(with `href` attr), `<button>`, a
|
|
|
111
111
|
}
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
- [Architecture](ARCHITECTURE.md) ([日本語](ARCHITECTURE.ja.md)) -- Package overview, module map, and class hierarchy
|
|
117
|
+
- [Selector Matching](docs/matching.md) ([日本語](docs/matching.ja.md)) -- CSS and regex selector matching algorithm details
|
|
118
|
+
- [Maintenance Guide](docs/maintenance.md) ([日本語](docs/maintenance.ja.md)) -- Commands, testing, recipes, and troubleshooting
|
|
119
|
+
|
|
114
120
|
## Install
|
|
115
121
|
|
|
116
122
|
[`markuplint`](https://www.npmjs.com/package/markuplint) package includes this package.
|