@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.
- package/ARCHITECTURE.ja.md +30 -14
- package/ARCHITECTURE.md +42 -26
- package/CHANGELOG.md +11 -0
- package/SKILL.md +1 -1
- package/docs/maintenance.ja.md +5 -3
- package/docs/maintenance.md +5 -3
- package/docs/matching.ja.md +2 -2
- package/docs/matching.md +2 -2
- package/lib/compare-specificity.d.ts +1 -1
- package/lib/create-selector.js +1 -1
- package/lib/extended-selector/aria-pseudo-class.d.ts +20 -3
- package/lib/extended-selector/aria-pseudo-class.js +27 -4
- package/lib/extended-selector/aria-role-pseudo-class.d.ts +2 -2
- package/lib/extended-selector/content-model-pseudo-class.d.ts +2 -2
- package/lib/index.d.ts +5 -2
- package/lib/index.js +1 -1
- package/lib/invalid-selector-error.js +3 -1
- package/lib/is.d.ts +8 -7
- package/lib/is.js +8 -11
- package/lib/match-selector.d.ts +4 -4
- package/lib/match-selector.js +17 -42
- package/lib/selector.d.ts +31 -6
- package/lib/selector.js +71 -97
- package/lib/types.d.ts +48 -2
- package/package.json +8 -5
package/ARCHITECTURE.ja.md
CHANGED
|
@@ -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
|
|
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
|
|
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 —
|
|
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`
|
|
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` |
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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 —
|
|
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
|
|
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` |
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
package/docs/maintenance.ja.md
CHANGED
|
@@ -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:
|
|
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`
|
|
211
|
+
- `jsdom` で作成された要素はセレクタマッチングロジックが使用する `SelectorElement` インターフェースを満たす
|
package/docs/maintenance.md
CHANGED
|
@@ -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:
|
|
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`
|
|
211
|
+
- Elements created via `jsdom` satisfy the `SelectorElement` interface used by the selector matching logic
|
package/docs/matching.ja.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
1. **CSS セレクタマッチング** -- `postcss-selector-parser` でパースされる標準 CSS セレクタ
|
|
10
10
|
2. **Regex セレクタマッチング** -- 正規表現を使用したパターンベースのマッチング
|
|
11
11
|
|
|
12
|
-
|
|
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):
|
|
10
|
+
export declare function compareSpecificity(a: Specificity, b: Specificity): 0 | 1 | -1;
|
package/lib/create-selector.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
|
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?.
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
5
|
+
* @param node - The node to check
|
|
5
6
|
* @returns `true` if the node is an Element
|
|
6
7
|
*/
|
|
7
|
-
export function isElement(
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
package/lib/match-selector.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
29
|
+
export declare function matchSelector(el: SelectorNode, selector: string | RegexSelector | undefined, scope?: SelectorNode | null, specs?: MLMLSpec): SelectorMatches;
|
|
30
30
|
export {};
|
package/lib/match-selector.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 } =
|
|
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 ${
|
|
144
|
+
throw new Error(`Unsupported ${this.#combinedFrom.combinator} combinator in selector`);
|
|
165
145
|
}
|
|
166
146
|
}
|
|
167
147
|
}
|
|
168
|
-
_matchWithoutCombineChecking(
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
29
|
-
__classPrivateFieldSet(this, _Selector_ruleset, Ruleset.parse(selector, extended), "f");
|
|
21
|
+
this.#ruleset = Ruleset.parse(selector, extended);
|
|
30
22
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
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 (
|
|
78
|
-
throw new InvalidSelectorError(
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
const 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(
|
|
121
|
-
|
|
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
|
-
|
|
130
|
+
this.#edge.add(node);
|
|
136
131
|
}
|
|
137
132
|
}
|
|
138
133
|
}
|
|
139
134
|
}
|
|
140
135
|
get selector() {
|
|
141
|
-
return
|
|
136
|
+
return this.#selector.nodes.join('');
|
|
142
137
|
}
|
|
143
|
-
match(
|
|
144
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (!
|
|
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 } =
|
|
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 ${
|
|
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 (!
|
|
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,
|
|
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
|
|
19
|
-
readonly nodes: readonly
|
|
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": "
|
|
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": "
|
|
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.
|
|
34
|
+
"type-fest": "5.4.4"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@types/jsdom": "27.0.0",
|
|
35
|
-
"jsdom": "
|
|
38
|
+
"jsdom": "28.1.0"
|
|
36
39
|
},
|
|
37
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "13dcfc84ec83d87360c720e253383b60767e1b56"
|
|
38
41
|
}
|