@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.
@@ -0,0 +1,211 @@
1
+ # セレクタマッチング
2
+
3
+ `@markuplint/selector` の 2 つのセレクタマッチングシステムの詳細ドキュメント。
4
+
5
+ ## 概要
6
+
7
+ このパッケージは 2 つの独立したマッチングシステムを提供します:
8
+
9
+ 1. **CSS セレクタマッチング** -- `postcss-selector-parser` でパースされる標準 CSS セレクタ
10
+ 2. **Regex セレクタマッチング** -- 正規表現を使用したパターンベースのマッチング
11
+
12
+ 両システムとも DOM `Element` ではなく純粋データインターフェース(`SelectorNode`/`SelectorElement`)上で動作し、詳細度情報を返し、統合関数 `matchSelector()` を通じて使用できます。
13
+
14
+ ## CSS セレクタマッチングフロー
15
+
16
+ ### 1. エントリーポイント
17
+
18
+ ```
19
+ createSelector(selectorString, specs?)
20
+ → new Selector(selectorString, extendedPseudoClasses)
21
+ → Ruleset.parse(selectorString, extended)
22
+ → postcss-selector-parser がセレクタ文字列を処理
23
+ → parser.Selector[] AST ノードを返却
24
+ ```
25
+
26
+ ### 2. パース
27
+
28
+ `Ruleset.parse()` は `postcss-selector-parser` を使用してセレクタ文字列を AST にパースします。カンマ区切りの各セレクタは `parser.Selector` ノードになります。`Ruleset` は各ノードを `StructuredSelector` でラップします。
29
+
30
+ ### 3. StructuredSelector チェーンの構築
31
+
32
+ 各 `StructuredSelector` は AST ノードを走査し、コンビネータで連結された `SelectorTarget` オブジェクトのチェーンを構築します:
33
+
34
+ ```
35
+ div > .class:not(.other) span
36
+ → SelectorTarget("div") → 子コンビネータ →
37
+ SelectorTarget(".class:not(.other)") → 子孫コンビネータ →
38
+ SelectorTarget("span")
39
+ ```
40
+
41
+ チェーンは AST から左から右に構築されますが、マッチングは右から左に行われます(現在の要素から開始)。
42
+
43
+ ### 4. SelectorTarget マッチング
44
+
45
+ 各 `SelectorTarget` は複合セレクタのコンポーネントを以下の順序でマッチングします:
46
+
47
+ 1. **名前空間チェック** -- 存在する場合、要素の名前空間を検証(`svg` と `*` のみサポート)
48
+ 2. **ID セレクタ**(`#id`)-- `el.id` にマッチ、詳細度 `[1, 0, 0]`
49
+ 3. **タグセレクタ**(`div`)-- `el.localName` にマッチ(純粋な HTML 要素は大文字小文字非区別)、詳細度 `[0, 0, 1]`。ユニバーサルセレクタ(`*`)はタグ型として扱われますが、詳細度は加算されません。
50
+ 4. **クラスセレクタ**(`.class`)-- `el.classList` にマッチ、詳細度 `[0, 1, 0]`
51
+ 5. **属性セレクタ**(`[attr=val]`)-- 要素属性を演算子付きでマッチ、詳細度 `[0, 1, 0]`
52
+ 6. **擬似クラス**(`:not()`、`:has()` 等)-- 専用ハンドラにディスパッチ
53
+
54
+ いずれかのコンポーネントがマッチに失敗すると、`SelectorTarget` 全体が失敗します(早期終了)。
55
+
56
+ ### 5. コンビネータマッチング
57
+
58
+ `SelectorTarget` がマッチすると、`StructuredSelector` はコンビネータに従って次のターゲットへ進みます:
59
+
60
+ | コンビネータ | 記号 | DOM トラバーサル |
61
+ | ------------ | --------------- | ----------------------------------------- |
62
+ | 子孫 | ` `(スペース) | `parentElement` チェーンをたどる |
63
+ | 子 | `>` | 直接の `parentElement` を確認 |
64
+ | 隣接兄弟 | `+` | `previousElementSibling` を確認 |
65
+ | 一般兄弟 | `~` | `previousElementSibling` チェーンをたどる |
66
+
67
+ ## 擬似クラスの処理
68
+
69
+ ### 標準擬似クラス
70
+
71
+ | 擬似クラス | 動作 |
72
+ | ------------------ | ---------------------------------------------------------------------------- |
73
+ | `:not(selector)` | 内部セレクタがマッチしない場合にマッチ。詳細度は内部セレクタに等しい。 |
74
+ | `:is(selector)` | いずれかの内部セレクタがマッチすればマッチ。詳細度は最も高いマッチに等しい。 |
75
+ | `:where(selector)` | `:is()` と同じだが、常に `[0, 0, 0]` の詳細度。 |
76
+ | `:has(selector)` | 子孫(またはコンビネータ `+`/`~` で兄弟)がマッチすればマッチ。 |
77
+ | `:scope` | スコープ要素にマッチ(スコープなしの場合はルート)。詳細度 `[0, 1, 0]`。 |
78
+ | `:root` | `<html>` 要素にマッチ。詳細度 `[0, 1, 0]`。 |
79
+
80
+ ### カスタム: `:closest(selector)`
81
+
82
+ 祖先チェーンをたどり、いずれかの祖先が内部セレクタにマッチすればマッチします。これは W3C 仕様にない markuplint の拡張です。
83
+
84
+ ### 拡張擬似クラス
85
+
86
+ 拡張擬似クラスは `ExtendedPseudoClass` レジストリを通じてディスパッチされます。ハンドラは `SelectorElement` を受け取り、`@markuplint/ml-spec` API を呼び出す際に `Element` にキャストします:
87
+
88
+ #### `:aria(syntax)`
89
+
90
+ | 構文 | 動作 |
91
+ | ------------- | --------------------------------------------------- |
92
+ | `has name` | `getAccname(el)` が空でない文字列を返す場合にマッチ |
93
+ | `has no name` | `getAccname(el)` が空文字列を返す場合にマッチ |
94
+
95
+ バージョン構文をサポート: `:aria(has name|1.2)`(バージョンパラメータはパースされますが、フィルタリングにはまだ使用されていません)。
96
+
97
+ #### `:role(roleName)` / `:role(roleName|version)`
98
+
99
+ `getComputedRole(specs, el, version)` が返すロールの `name` が指定された `roleName` と一致する場合にマッチします。バージョンのデフォルトは `ARIA_RECOMMENDED_VERSION` です。
100
+
101
+ #### `:model(category)`
102
+
103
+ 指定された HTML コンテンツモデルカテゴリに要素が属する場合にマッチします。`contentModelCategoryToTagNames()` を使用してカテゴリのマッチングセレクタリストを取得し、各セレクタを要素に対してテストします。
104
+
105
+ 特殊ケース:
106
+
107
+ - `#custom` -- カスタム要素(`isCustomElement` プロパティを持つ要素)にマッチ
108
+ - `#text` -- 常にマッチしない(テキストノードは要素ではない)
109
+
110
+ ## Regex セレクタマッチングフロー
111
+
112
+ ### 1. エントリーポイント
113
+
114
+ ```
115
+ matchSelector(el, regexSelector)
116
+ → regexSelect(el, regexSelector)
117
+ → combination リンクから SelectorTarget チェーンを構築
118
+ → エッジ(最深の combination)からルートへ向かってマッチング
119
+ ```
120
+
121
+ ### 2. SelectorTarget チェーンの構築
122
+
123
+ `RegexSelector` 型はチェーンされた combination をサポートします:
124
+
125
+ ```typescript
126
+ {
127
+ nodeName: "/^div$/",
128
+ combination: {
129
+ combinator: ">",
130
+ nodeName: "/^span$/",
131
+ combination: {
132
+ combinator: "+",
133
+ attrName: "/^data-/"
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ これにより次のチェーンが構築されます: `SelectorTarget(div) → > → SelectorTarget(span) → + → SelectorTarget([data-*])`
140
+
141
+ ### 3. パターンマッチング
142
+
143
+ `regexSelectorMatches(pattern, value, ignoreCase)` がパターンマッチングを処理します:
144
+
145
+ - **プレーン文字列**: `^pattern$` としてラップ(完全一致)
146
+ - **正規表現リテラル**(`/pattern/flags`): 指定されたフラグでそのまま使用
147
+ - **大文字小文字の区別**: HTML 要素は大文字小文字を区別しないマッチングを使用(`isPureHTMLElement()` の場合 `ignoreCase = true`)
148
+
149
+ ### 4. Regex コンビネータ
150
+
151
+ 標準 CSS コンビネータに加え、2 つの追加コンビネータをサポートします:
152
+
153
+ | コンビネータ | 記号 | DOM トラバーサル |
154
+ | ------------ | ----------- | ----------------------------------------- |
155
+ | 子孫 | `' '` | `parentElement` チェーンをたどる |
156
+ | 子 | `'>'` | 直接の `parentElement` を確認 |
157
+ | 隣接兄弟 | `'+'` | `previousElementSibling` を確認 |
158
+ | 一般兄弟 | `'~'` | `previousElementSibling` チェーンをたどる |
159
+ | 前方隣接兄弟 | `':has(+)'` | `nextElementSibling` を確認 |
160
+ | 前方一般兄弟 | `':has(~)'` | `nextElementSibling` チェーンをたどる |
161
+
162
+ ### 5. データキャプチャ
163
+
164
+ マッチした正規表現キャプチャグループは `data` オブジェクトに収集されます:
165
+
166
+ ```typescript
167
+ // パターン: "/^(?<prefix>[a-z]+)-(?<suffix>[a-z]+)$/"
168
+ // 値: "data-value"
169
+ // 結果: { $0: "data-value", $1: "data", $2: "value", prefix: "data", suffix: "value" }
170
+ ```
171
+
172
+ `nodeName` マッチングの `$0` キャプチャは削除されます(完全一致であり、要素名と冗長なため)。チェーン内の全ターゲットのデータはマージされます。
173
+
174
+ ### 6. 詳細度の計算
175
+
176
+ Regex セレクタの詳細度はターゲットごとに計算されます:
177
+
178
+ - `nodeName` マッチ: `[0, 0, 1]`(タイプ詳細度)
179
+ - マッチした各属性: `[0, 1, 0]`(クラスレベル詳細度)
180
+ - 結合されたターゲットの詳細度は合算されます
181
+
182
+ ## キャッシュ
183
+
184
+ `createSelector()` は `Map<string, Selector>` キャッシュを保持します。同じセレクタ文字列での後続の呼び出しは同じ `Selector` インスタンスを返し、`postcss-selector-parser` による繰り返しのパースを回避します。
185
+
186
+ ## サポート対象・非対象セレクタ
187
+
188
+ ### サポート対象
189
+
190
+ - ユニバーサル(`*`)、タイプ(`div`)、ID(`#id`)、クラス(`.class`)
191
+ - 全属性セレクタ演算子(`=`、`~=`、`|=`、`*=`、`^=`、`$=`、大文字小文字非区別 `i` フラグ)
192
+ - コンビネータ: 子孫(` `)、子(`>`)、隣接兄弟(`+`)、一般兄弟(`~`)
193
+ - 複数セレクタ(`,`)
194
+ - `:not()`、`:is()`、`:where()`、`:has()`、`:scope`、`:root`
195
+ - `:closest()`(markuplint 拡張)
196
+ - 拡張: `:aria()`、`:role()`、`:model()`
197
+ - 名前空間セレクタ(`svg|text`、`*|div`)。注: `svg` と `*` の名前空間のみサポート。他の名前空間(例: `html`)は `InvalidSelectorError` をスローします。
198
+
199
+ ### 非サポート(エラーをスロー)
200
+
201
+ 構造擬似クラス: `:empty`、`:nth-child()`、`:nth-last-child()`、`:first-child`、`:last-child`、`:only-child`、`:nth-of-type()`、`:nth-last-of-type()`、`:first-of-type`、`:last-of-type`、`:only-of-type`、`:nth-col()`、`:nth-last-col()`
202
+
203
+ 入力擬似クラス: `:enable`、`:disable`、`:read-write`、`:read-only`、`:placeholder-shown`、`:default`、`:checked`、`:indeterminate`、`:valid`、`:invalid`、`:in-range`、`:out-of-range`、`:required`、`:optional`、`:blank`、`:user-invalid`
204
+
205
+ ### 無視(エラーをスロー)
206
+
207
+ ユーザーインタラクション/動的擬似クラス: `:dir()`、`:lang()`、`:any-link`、`:link`、`:visited`、`:local-link`、`:target`、`:target-within`、`:current`、`:past`、`:future`、`:active`、`:hover`、`:focus`、`:focus-within`、`:focus-visible`
208
+
209
+ 擬似要素: `::before`、`::after`
210
+
211
+ カラムコンビネータ: `||`
@@ -0,0 +1,211 @@
1
+ # Selector Matching
2
+
3
+ Detailed documentation of the two selector matching systems in `@markuplint/selector`.
4
+
5
+ ## Overview
6
+
7
+ The package provides two independent matching systems:
8
+
9
+ 1. **CSS Selector Matching** -- Standard CSS selectors parsed via `postcss-selector-parser`
10
+ 2. **Regex Selector Matching** -- Pattern-based matching using regular expressions
11
+
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
+
14
+ ## CSS Selector Matching Flow
15
+
16
+ ### 1. Entry Point
17
+
18
+ ```
19
+ createSelector(selectorString, specs?)
20
+ → new Selector(selectorString, extendedPseudoClasses)
21
+ → Ruleset.parse(selectorString, extended)
22
+ → postcss-selector-parser processes the string
23
+ → Returns parser.Selector[] AST nodes
24
+ ```
25
+
26
+ ### 2. Parsing
27
+
28
+ `Ruleset.parse()` uses `postcss-selector-parser` to parse the selector string into an AST. Each comma-separated selector becomes a `parser.Selector` node. The `Ruleset` wraps each one in a `StructuredSelector`.
29
+
30
+ ### 3. StructuredSelector Chain Building
31
+
32
+ Each `StructuredSelector` walks the AST nodes and builds a chain of `SelectorTarget` objects linked by combinators:
33
+
34
+ ```
35
+ div > .class:not(.other) span
36
+ → SelectorTarget("div") → child combinator →
37
+ SelectorTarget(".class:not(.other)") → descendant combinator →
38
+ SelectorTarget("span")
39
+ ```
40
+
41
+ The chain is built left-to-right from the AST but matched right-to-left (starting from the current element).
42
+
43
+ ### 4. SelectorTarget Matching
44
+
45
+ Each `SelectorTarget` matches its compound selector components in this order:
46
+
47
+ 1. **Namespace check** -- If present, validates the element's namespace (only `svg` and `*` are supported)
48
+ 2. **ID selector** (`#id`) -- Matches `el.id`, specificity `[1, 0, 0]`
49
+ 3. **Tag selector** (`div`) -- Matches `el.localName` (case-insensitive for pure HTML elements), specificity `[0, 0, 1]`. Universal selector (`*`) is handled as a tag type but adds no specificity.
50
+ 4. **Class selector** (`.class`) -- Matches `el.classList`, specificity `[0, 1, 0]`
51
+ 5. **Attribute selector** (`[attr=val]`) -- Matches element attributes with operator support, specificity `[0, 1, 0]`
52
+ 6. **Pseudo-class** (`:not()`, `:has()`, etc.) -- Dispatched to specialized handlers
53
+
54
+ If any component fails to match, the entire `SelectorTarget` fails (early termination).
55
+
56
+ ### 5. Combinator Matching
57
+
58
+ When a `SelectorTarget` matches, the `StructuredSelector` follows the combinator to the next target:
59
+
60
+ | Combinator | Symbol | DOM Traversal |
61
+ | ------------------ | ----------- | ------------------------------------------------ |
62
+ | Descendant | ` ` (space) | Walk up through `parentElement` chain |
63
+ | Child | `>` | Check immediate `parentElement` |
64
+ | Next-sibling | `+` | Check `previousElementSibling` |
65
+ | Subsequent-sibling | `~` | Walk back through `previousElementSibling` chain |
66
+
67
+ ## Pseudo-Class Handling
68
+
69
+ ### Standard Pseudo-Classes
70
+
71
+ | Pseudo-Class | Behavior |
72
+ | ------------------ | -------------------------------------------------------------------------------------- |
73
+ | `:not(selector)` | Matches if the inner selector does NOT match. Specificity equals the inner selector's. |
74
+ | `:is(selector)` | Matches if ANY inner selector matches. Specificity equals the most specific match. |
75
+ | `:where(selector)` | Same as `:is()` but always contributes `[0, 0, 0]` specificity. |
76
+ | `:has(selector)` | Matches if a descendant (or sibling with `+`/`~` combinator) matches. |
77
+ | `:scope` | Matches the scope element (or root if no scope). Specificity `[0, 1, 0]`. |
78
+ | `:root` | Matches the `<html>` element. Specificity `[0, 1, 0]`. |
79
+
80
+ ### Custom: `:closest(selector)`
81
+
82
+ Walks up the ancestor chain and matches if any ancestor matches the inner selector. This is a markuplint extension not in the W3C specification.
83
+
84
+ ### Extended Pseudo-Classes
85
+
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
+
88
+ #### `:aria(syntax)`
89
+
90
+ | Syntax | Behavior |
91
+ | ------------- | ------------------------------------------------------ |
92
+ | `has name` | Matches if `getAccname(el)` returns a non-empty string |
93
+ | `has no name` | Matches if `getAccname(el)` returns an empty string |
94
+
95
+ Supports version syntax: `:aria(has name|1.2)` (version parameter is parsed but not yet used for filtering).
96
+
97
+ #### `:role(roleName)` / `:role(roleName|version)`
98
+
99
+ Matches if `getComputedRole(specs, el, version)` returns a role whose `name` equals the specified `roleName`. The version defaults to `ARIA_RECOMMENDED_VERSION`.
100
+
101
+ #### `:model(category)`
102
+
103
+ Matches if the element belongs to the specified HTML content model category. Uses `contentModelCategoryToTagNames()` to get the list of matching selectors for the category, then tests each against the element.
104
+
105
+ Special cases:
106
+
107
+ - `#custom` -- Matches custom elements (elements with `isCustomElement` property)
108
+ - `#text` -- Always returns unmatched (text nodes are not elements)
109
+
110
+ ## Regex Selector Matching Flow
111
+
112
+ ### 1. Entry Point
113
+
114
+ ```
115
+ matchSelector(el, regexSelector)
116
+ → regexSelect(el, regexSelector)
117
+ → Builds SelectorTarget chain from combination links
118
+ → Matches from the edge (deepest combination) back to root
119
+ ```
120
+
121
+ ### 2. SelectorTarget Chain Building
122
+
123
+ The `RegexSelector` type supports chained combinations:
124
+
125
+ ```typescript
126
+ {
127
+ nodeName: "/^div$/",
128
+ combination: {
129
+ combinator: ">",
130
+ nodeName: "/^span$/",
131
+ combination: {
132
+ combinator: "+",
133
+ attrName: "/^data-/"
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ This builds a chain: `SelectorTarget(div) → > → SelectorTarget(span) → + → SelectorTarget([data-*])`.
140
+
141
+ ### 3. Pattern Matching
142
+
143
+ `regexSelectorMatches(pattern, value, ignoreCase)` handles pattern matching:
144
+
145
+ - **Plain string**: Wrapped as `^pattern$` (exact match)
146
+ - **Regex literal** (`/pattern/flags`): Used as-is with the specified flags
147
+ - **Case sensitivity**: HTML elements use case-insensitive matching (`ignoreCase = true` when `isPureHTMLElement()`)
148
+
149
+ ### 4. Regex Combinators
150
+
151
+ Standard CSS combinators are supported, plus two extra:
152
+
153
+ | Combinator | Symbol | DOM Traversal |
154
+ | -------------------- | ----------- | ------------------------------------------------ |
155
+ | Descendant | `' '` | Walk up `parentElement` chain |
156
+ | Child | `'>'` | Check immediate `parentElement` |
157
+ | Next-sibling | `'+'` | Check `previousElementSibling` |
158
+ | Subsequent-sibling | `'~'` | Walk back through `previousElementSibling` chain |
159
+ | Prev-sibling | `':has(+)'` | Check `nextElementSibling` |
160
+ | Subsequent (forward) | `':has(~)'` | Walk forward through `nextElementSibling` chain |
161
+
162
+ ### 5. Data Capture
163
+
164
+ Matched regex capture groups are collected into a `data` object:
165
+
166
+ ```typescript
167
+ // Pattern: "/^(?<prefix>[a-z]+)-(?<suffix>[a-z]+)$/"
168
+ // Value: "data-value"
169
+ // Result: { $0: "data-value", $1: "data", $2: "value", prefix: "data", suffix: "value" }
170
+ ```
171
+
172
+ The `$0` capture from `nodeName` matching is deleted (it's the full match, redundant with the element name). Data from all targets in the chain is merged.
173
+
174
+ ### 6. Specificity Calculation
175
+
176
+ Regex selector specificity is calculated per target:
177
+
178
+ - `nodeName` match: `[0, 0, 1]` (type specificity)
179
+ - Each matched attribute: `[0, 1, 0]` (class-level specificity)
180
+ - Specificity from combined targets is summed
181
+
182
+ ## Caching
183
+
184
+ `createSelector()` maintains a `Map<string, Selector>` cache. Subsequent calls with the same selector string return the same `Selector` instance, avoiding repeated parsing by `postcss-selector-parser`.
185
+
186
+ ## Supported vs Unsupported Selectors
187
+
188
+ ### Supported
189
+
190
+ - Universal (`*`), type (`div`), ID (`#id`), class (`.class`)
191
+ - All attribute selector operators (`=`, `~=`, `|=`, `*=`, `^=`, `$=`, case-insensitive `i` flag)
192
+ - Combinators: descendant (` `), child (`>`), next-sibling (`+`), subsequent-sibling (`~`)
193
+ - Multiple selectors (`,`)
194
+ - `:not()`, `:is()`, `:where()`, `:has()`, `:scope`, `:root`
195
+ - `:closest()` (markuplint extension)
196
+ - Extended: `:aria()`, `:role()`, `:model()`
197
+ - Namespace selectors (`svg|text`, `*|div`). Note: only `svg` and `*` namespaces are supported; other namespaces (e.g., `html`) throw `InvalidSelectorError`.
198
+
199
+ ### Unsupported (throws error)
200
+
201
+ Structural pseudo-classes: `:empty`, `:nth-child()`, `:nth-last-child()`, `:first-child`, `:last-child`, `:only-child`, `:nth-of-type()`, `:nth-last-of-type()`, `:first-of-type`, `:last-of-type`, `:only-of-type`, `:nth-col()`, `:nth-last-col()`
202
+
203
+ Input pseudo-classes: `:enable`, `:disable`, `:read-write`, `:read-only`, `:placeholder-shown`, `:default`, `:checked`, `:indeterminate`, `:valid`, `:invalid`, `:in-range`, `:out-of-range`, `:required`, `:optional`, `:blank`, `:user-invalid`
204
+
205
+ ### Ignored (throws error)
206
+
207
+ User interaction / dynamic pseudo-classes: `:dir()`, `:lang()`, `:any-link`, `:link`, `:visited`, `:local-link`, `:target`, `:target-within`, `:current`, `:past`, `:future`, `:active`, `:hover`, `:focus`, `:focus-within`, `:focus-visible`
208
+
209
+ Pseudo-elements: `::before`, `::after`
210
+
211
+ Column combinator: `||`
@@ -1,2 +1,10 @@
1
1
  import type { Specificity } from './types.js';
2
- export declare function compareSpecificity(a: Specificity, b: Specificity): 1 | -1 | 0;
2
+ /**
3
+ * Compares two CSS specificity tuples using the standard comparison algorithm.
4
+ * Compares from left (ID) to right (type) component.
5
+ *
6
+ * @param a - The first specificity tuple `[id, class, type]`
7
+ * @param b - The second specificity tuple `[id, class, type]`
8
+ * @returns `-1` if `a` is less specific, `1` if `a` is more specific, `0` if equal
9
+ */
10
+ export declare function compareSpecificity(a: Specificity, b: Specificity): 0 | 1 | -1;
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Compares two CSS specificity tuples using the standard comparison algorithm.
3
+ * Compares from left (ID) to right (type) component.
4
+ *
5
+ * @param a - The first specificity tuple `[id, class, type]`
6
+ * @param b - The second specificity tuple `[id, class, type]`
7
+ * @returns `-1` if `a` is less specific, `1` if `a` is more specific, `0` if equal
8
+ */
1
9
  export function compareSpecificity(a, b) {
2
10
  if (a[0] < b[0]) {
3
11
  return -1;
@@ -1,3 +1,16 @@
1
1
  import type { MLMLSpec } from '@markuplint/ml-spec';
2
2
  import { Selector } from './selector.js';
3
+ /**
4
+ * Creates a cached {@link Selector} instance for the given CSS selector string.
5
+ *
6
+ * Results are cached by selector string so subsequent calls with the same
7
+ * selector return the same instance.
8
+ *
9
+ * When `specs` is provided, markuplint's extended pseudo-classes
10
+ * (`:model()`, `:aria()`, `:role()`) are available.
11
+ *
12
+ * @param selector - The CSS selector string to parse
13
+ * @param specs - Optional HTML/ARIA specification data for extended pseudo-classes
14
+ * @returns A reusable Selector instance
15
+ */
3
16
  export declare function createSelector(selector: string, specs?: MLMLSpec): Selector;
@@ -3,6 +3,19 @@ import { ariaRolePseudoClass } from './extended-selector/aria-role-pseudo-class.
3
3
  import { contentModelPseudoClass } from './extended-selector/content-model-pseudo-class.js';
4
4
  import { Selector } from './selector.js';
5
5
  const caches = new Map();
6
+ /**
7
+ * Creates a cached {@link Selector} instance for the given CSS selector string.
8
+ *
9
+ * Results are cached by selector string so subsequent calls with the same
10
+ * selector return the same instance.
11
+ *
12
+ * When `specs` is provided, markuplint's extended pseudo-classes
13
+ * (`:model()`, `:aria()`, `:role()`) are available.
14
+ *
15
+ * @param selector - The CSS selector string to parse
16
+ * @param specs - Optional HTML/ARIA specification data for extended pseudo-classes
17
+ * @returns A reusable Selector instance
18
+ */
6
19
  export function createSelector(selector, specs) {
7
20
  let instance = caches.get(selector);
8
21
  if (instance) {
@@ -11,7 +24,7 @@ export function createSelector(selector, specs) {
11
24
  instance = new Selector(selector, specs
12
25
  ? {
13
26
  model: contentModelPseudoClass(specs),
14
- aria: ariaPseudoClass(),
27
+ aria: ariaPseudoClass(specs),
15
28
  role: ariaRolePseudoClass(specs),
16
29
  }
17
30
  : undefined);
package/lib/debug.d.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  import debug from 'debug';
2
+ /** Debug logger instance for the `selector` namespace. */
2
3
  export declare const log: debug.Debugger;
4
+ /**
5
+ * Enables debug logging for the selector and markuplint-cli namespaces.
6
+ * Once enabled, logs are output via the `debug` package.
7
+ */
3
8
  export declare function enableDebug(): void;
package/lib/debug.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import debug from 'debug';
2
2
  const CLI_NS = 'markuplint-cli';
3
+ /** Debug logger instance for the `selector` namespace. */
3
4
  export const log = debug('selector');
5
+ /**
6
+ * Enables debug logging for the selector and markuplint-cli namespaces.
7
+ * Once enabled, logs are output via the `debug` package.
8
+ */
4
9
  export function enableDebug() {
5
10
  if (!log.enabled) {
6
11
  debug.enable(`${log.namespace}*`);
@@ -1,5 +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
- * Version Syntax is not support yet.
4
+ * Creates the `:aria()` extended pseudo-class handler.
5
+ *
6
+ * Matches elements by accessible name presence.
7
+ * Supports `has name` and `has no name` syntax.
8
+ * Version syntax is parsed but not yet used for filtering.
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
26
+ * @returns An extended pseudo-class handler function
4
27
  */
5
- export declare function ariaPseudoClass(): (content: string) => (el: Element) => SelectorResult;
28
+ export declare function ariaPseudoClass(specs: MLMLSpec): (content: string) => (el: SelectorElement) => SelectorResult;
@@ -1,13 +1,42 @@
1
1
  import { validateAriaVersion, ARIA_RECOMMENDED_VERSION, getAccname } from '@markuplint/ml-spec';
2
2
  /**
3
- * Version Syntax is not support yet.
3
+ * Creates the `:aria()` extended pseudo-class handler.
4
+ *
5
+ * Matches elements by accessible name presence.
6
+ * Supports `has name` and `has no name` syntax.
7
+ * Version syntax is parsed but not yet used for filtering.
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
25
+ * @returns An extended pseudo-class handler function
4
26
  */
5
- export function ariaPseudoClass() {
27
+ export function ariaPseudoClass(specs) {
6
28
  return (content) => (
7
29
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
8
30
  el) => {
9
31
  const aria = ariaPseudoClassParser(content);
10
- 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);
11
40
  switch (aria.type) {
12
41
  case 'hasName': {
13
42
  if (name) {
@@ -42,7 +71,7 @@ export function ariaPseudoClass() {
42
71
  }
43
72
  function ariaPseudoClassParser(syntax) {
44
73
  const [_query, _version] = syntax.split('|');
45
- const query = _query?.replace(/\s+/g, '').toLowerCase();
74
+ const query = _query?.replaceAll(/\s+/g, '').toLowerCase();
46
75
  const version = _version ?? ARIA_RECOMMENDED_VERSION;
47
76
  if (!validateAriaVersion(version)) {
48
77
  throw new SyntaxError(`Unsupported ARIA version: ${version}`);
@@ -1,3 +1,12 @@
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
- export declare function ariaRolePseudoClass(specs: MLMLSpec): (content: string) => (el: Element) => SelectorResult;
3
+ /**
4
+ * Creates the `:role()` extended pseudo-class handler.
5
+ *
6
+ * Matches elements whose computed ARIA role equals the specified role name.
7
+ * Supports version syntax: `:role(roleName|version)`.
8
+ *
9
+ * @param specs - The HTML/ARIA specification data used for role computation
10
+ * @returns An extended pseudo-class handler function
11
+ */
12
+ export declare function ariaRolePseudoClass(specs: MLMLSpec): (content: string) => (el: SelectorElement) => SelectorResult;
@@ -1,4 +1,13 @@
1
1
  import { validateAriaVersion, ARIA_RECOMMENDED_VERSION, getComputedRole } from '@markuplint/ml-spec';
2
+ /**
3
+ * Creates the `:role()` extended pseudo-class handler.
4
+ *
5
+ * Matches elements whose computed ARIA role equals the specified role name.
6
+ * Supports version syntax: `:role(roleName|version)`.
7
+ *
8
+ * @param specs - The HTML/ARIA specification data used for role computation
9
+ * @returns An extended pseudo-class handler function
10
+ */
2
11
  export function ariaRolePseudoClass(specs) {
3
12
  return (content) => (
4
13
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1,3 +1,12 @@
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
- export declare function contentModelPseudoClass(specs: MLMLSpec): (category: string) => (el: Element) => SelectorResult;
3
+ /**
4
+ * Creates the `:model()` extended pseudo-class handler.
5
+ *
6
+ * Matches elements that belong to the specified HTML content model category
7
+ * (e.g., `interactive`, `phrasing`, `flow`).
8
+ *
9
+ * @param specs - The HTML/ARIA specification data containing content model definitions
10
+ * @returns An extended pseudo-class handler function
11
+ */
12
+ export declare function contentModelPseudoClass(specs: MLMLSpec): (category: string) => (el: SelectorElement) => SelectorResult;
@@ -1,5 +1,14 @@
1
1
  import { contentModelCategoryToTagNames } from '@markuplint/ml-spec';
2
2
  import { createSelector } from '../create-selector.js';
3
+ /**
4
+ * Creates the `:model()` extended pseudo-class handler.
5
+ *
6
+ * Matches elements that belong to the specified HTML content model category
7
+ * (e.g., `interactive`, `phrasing`, `flow`).
8
+ *
9
+ * @param specs - The HTML/ARIA specification data containing content model definitions
10
+ * @returns An extended pseudo-class handler function
11
+ */
3
12
  export function contentModelPseudoClass(specs) {
4
13
  return (category) => (
5
14
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types