@markuplint/ml-spec 4.10.2 → 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 +24 -10
- package/ARCHITECTURE.md +24 -10
- package/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/docs/aria-algorithms.ja.md +180 -33
- package/docs/aria-algorithms.md +183 -34
- package/docs/html-algorithms.ja.md +2 -2
- package/docs/html-algorithms.md +2 -2
- package/docs/maintenance.ja.md +13 -22
- package/docs/maintenance.md +13 -22
- package/docs/spec-resolution.ja.md +37 -9
- package/docs/spec-resolution.md +45 -16
- package/docs/type-definitions.ja.md +8 -4
- package/docs/type-definitions.md +8 -4
- package/lib/algorithm/aria/accname/__tests__/test-helpers.d.ts +45 -0
- package/lib/algorithm/aria/accname/__tests__/test-helpers.js +120 -0
- package/lib/algorithm/aria/accname/aria-steps.d.ts +51 -0
- package/lib/algorithm/aria/accname/aria-steps.js +104 -0
- package/lib/algorithm/aria/accname/compute.d.ts +51 -0
- package/lib/algorithm/aria/accname/compute.js +101 -0
- package/lib/algorithm/aria/accname/element-names.d.ts +36 -0
- package/lib/algorithm/aria/accname/element-names.js +342 -0
- package/lib/algorithm/aria/accname/helpers.d.ts +98 -0
- package/lib/algorithm/aria/accname/helpers.js +330 -0
- package/lib/algorithm/aria/accname/index.d.ts +4 -0
- package/lib/algorithm/aria/accname/index.js +3 -0
- package/lib/algorithm/aria/accname/label-steps.d.ts +25 -0
- package/lib/algorithm/aria/accname/label-steps.js +66 -0
- package/lib/algorithm/aria/accname/svg-helpers.d.ts +17 -0
- package/lib/algorithm/aria/accname/svg-helpers.js +30 -0
- package/lib/algorithm/aria/accname/types.d.ts +70 -0
- package/lib/algorithm/aria/accname/types.js +2 -0
- package/lib/algorithm/aria/accname-computation.d.ts +14 -3
- package/lib/algorithm/aria/accname-computation.js +131 -8
- package/lib/algorithm/aria/aria-specs.d.ts +1 -0
- package/lib/algorithm/aria/get-aria.js +30 -4
- package/lib/algorithm/aria/get-computed-role.js +106 -26
- package/lib/algorithm/aria/get-explicit-role.d.ts +12 -0
- package/lib/algorithm/aria/get-explicit-role.js +12 -0
- package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +12 -0
- package/lib/algorithm/aria/get-non-presentational-ancestor.js +14 -2
- package/lib/algorithm/aria/get-permitted-roles-spec.d.ts +8 -4
- package/lib/algorithm/aria/get-permitted-roles-spec.js +19 -6
- package/lib/algorithm/aria/get-role-spec.js +10 -3
- package/lib/algorithm/aria/has-required-owned-elements.d.ts +2 -1
- package/lib/algorithm/aria/has-required-owned-elements.js +18 -15
- package/lib/algorithm/aria/is-presentational.d.ts +24 -0
- package/lib/algorithm/aria/is-presentational.js +31 -0
- package/lib/algorithm/aria/matches-context-role.d.ts +12 -4
- package/lib/algorithm/aria/matches-context-role.js +39 -6
- package/lib/algorithm/html/content-model-category-to-tag-names.js +1 -1
- package/lib/algorithm/html/get-content-model.d.ts +4 -2
- package/lib/algorithm/html/get-content-model.js +6 -7
- package/lib/const/accname.d.ts +29 -0
- package/lib/const/accname.js +76 -0
- package/lib/const/dom.d.ts +8 -0
- package/lib/const/dom.js +8 -0
- package/lib/const/index.d.ts +2 -0
- package/lib/const/index.js +2 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +4 -0
- package/lib/types/index.d.ts +65 -0
- package/lib/utils/directive-resolver.d.ts +23 -0
- package/lib/utils/directive-resolver.js +50 -0
- package/lib/utils/get-attr-specs-spec.js +4 -3
- package/lib/utils/get-ns.d.ts +7 -0
- package/lib/utils/get-ns.js +7 -0
- package/lib/utils/get-spec-by-tag-name.d.ts +1 -1
- package/lib/utils/merge-array.d.ts +10 -0
- package/lib/utils/merge-array.js +10 -0
- package/lib/utils/resolve-version.d.ts +11 -0
- package/lib/utils/resolve-version.js +11 -0
- package/lib/utils/schema-to-spec.d.ts +2 -0
- package/lib/utils/schema-to-spec.js +15 -4
- package/package.json +9 -7
|
@@ -107,7 +107,7 @@ gAttrs['#HTMLGlobalAttrs'] = {
|
|
|
107
107
|
#### 3. ARIA定義
|
|
108
108
|
|
|
109
109
|
各ARIAバージョン(`1.1`, `1.2`, `1.3`)について、`roles`, `props`,
|
|
110
|
-
`graphicsRoles` の
|
|
110
|
+
`graphicsRoles`, `dpubRoles` の4つの配列が `mergeArray` を使ってマージされます:
|
|
111
111
|
|
|
112
112
|
```ts
|
|
113
113
|
def['#aria'] = {
|
|
@@ -115,6 +115,7 @@ def['#aria'] = {
|
|
|
115
115
|
roles: mergeArray(def['#aria']['1.1'].roles, extendedSpec.def['#aria']['1.1'].roles),
|
|
116
116
|
props: mergeArray(def['#aria']['1.1'].props, extendedSpec.def['#aria']['1.1'].props),
|
|
117
117
|
graphicsRoles: mergeArray(def['#aria']['1.1'].graphicsRoles, extendedSpec.def['#aria']['1.1'].graphicsRoles),
|
|
118
|
+
dpubRoles: mergeArray(def['#aria']['1.1'].dpubRoles, extendedSpec.def['#aria']['1.1'].dpubRoles),
|
|
118
119
|
},
|
|
119
120
|
// 1.2 と 1.3 も同様
|
|
120
121
|
};
|
|
@@ -142,7 +143,35 @@ for (const modelName of keys) {
|
|
|
142
143
|
(例: `#phrasing` に `<router-link>` を追加)、
|
|
143
144
|
まったく新しいカテゴリを定義したりできます。
|
|
144
145
|
|
|
145
|
-
#### 5.
|
|
146
|
+
#### 5. ディレクティブパターン
|
|
147
|
+
|
|
148
|
+
拡張仕様が `directivePatterns` を提供する場合、既存の配列に連結されます:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
result.directivePatterns = [...(result.directivePatterns ?? []), ...extendedSpec.directivePatterns];
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
単純な配列結合です。コアエンジンはパターンを順番に評価します
|
|
155
|
+
(最初のマッチが優先)。そのため、フレームワーク spec はパターンを
|
|
156
|
+
最も具体的なものから最も一般的なものの順で定義する必要があります。
|
|
157
|
+
|
|
158
|
+
#### 6. `useIDLAttributeNames`
|
|
159
|
+
|
|
160
|
+
拡張仕様が `useIDLAttributeNames` を明示的に設定している場合(`true` または
|
|
161
|
+
`false`)、現在の値を上書きします:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
if (extendedSpec.useIDLAttributeNames != null) {
|
|
165
|
+
result.useIDLAttributeNames = extendedSpec.useIDLAttributeNames;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
これは明示的な `null` ガード付きの last-write-wins セマンティクスです。
|
|
170
|
+
プロパティを省略しても以前に設定された値はリセットされません。
|
|
171
|
+
このフラグは `@markuplint/ml-core` の `MLAttr` コンストラクタで使用され、
|
|
172
|
+
IDL-コンテンツ属性名解決(例: `className` → `class`)を有効化します。
|
|
173
|
+
|
|
174
|
+
#### 7. 要素仕様
|
|
146
175
|
|
|
147
176
|
要素は名前で照合されます(大文字小文字を区別しない比較)。
|
|
148
177
|
ベース仕様の各要素について:
|
|
@@ -479,7 +508,7 @@ function getName(def: NamedDefinition): string {
|
|
|
479
508
|
| --- | --------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------- | ------------------------------------------------- |
|
|
480
509
|
| 1 | `getSpecByTagName` | `Map<string, any>` | 名前空間修飾名(例: `"svg:circle"`) | `ElementSpec \| null` | モジュールライフタイム(クリアなし) |
|
|
481
510
|
| 2 | `getVersionResolvedARIA`(`get-aria.ts` 内) | `Map<string, ARIA \| null>` | `localName + namespace + version`(文字列連結) | バージョン解決済みARIA仕様またはnull | モジュールライフタイム(クリアなし) |
|
|
482
|
-
| 3 | `getContentModel` | `
|
|
511
|
+
| 3 | `getContentModel` | `WeakMap<Element, ...>` | DOM Element参照 | `PermittedContentPattern[] \| boolean \| null` | ElementがGCされるとエントリも自動削除 |
|
|
483
512
|
| 4 | `contentModelCategoryToTagNames` | `Map<Category, ReadonlyArray<string>>` | カテゴリ文字列(例: `"#flow"`) | フリーズされたソート済みタグ名配列 | モジュールライフタイム(クリアなし) |
|
|
484
513
|
| 5 | `resolveNamespace` | `Map<string, NamespacedElementName>` | `name + namespaceURI`(文字列連結) | 解決済み名前空間オブジェクト | モジュールライフタイム(クリアなし) |
|
|
485
514
|
| 6 | `getAttrSpecs`(`get-attr-specs-spec.ts` 内) | `Map<string, readonly Attribute[] \| null>` + `WeakSet<MLMLSpec>` | 名前空間修飾名 | ソート済み属性配列またはnull | スキーマ参照変更時にクリア(WeakSetチェック経由) |
|
|
@@ -504,12 +533,11 @@ if (!schemaCache.has(schema)) {
|
|
|
504
533
|
}
|
|
505
534
|
```
|
|
506
535
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
同じ仕様コンテキスト内で同じ要素を再クエリするとO(1)になります。
|
|
536
|
+
**WeakMapキャッシュ:** `getContentModel` キャッシュ(項目3)は
|
|
537
|
+
`WeakMap<Element, ...>` を使用し、DOM Element参照をキーとします。
|
|
538
|
+
同じ要素を再クエリするとO(1)になります。
|
|
539
|
+
要素がガベージコレクションされると(例: 再パース後)、
|
|
540
|
+
キャッシュエントリも自動的に削除され、メモリリークを防止します。
|
|
513
541
|
|
|
514
542
|
**決定論的キー:** 項目1、2、5のキャッシュは文字列連結をキーに使用します。
|
|
515
543
|
`resolveNamespace` は同じ入力に対して決定論的な出力を生成し、
|
package/docs/spec-resolution.md
CHANGED
|
@@ -111,8 +111,8 @@ the extension.
|
|
|
111
111
|
|
|
112
112
|
#### 3. ARIA Definitions
|
|
113
113
|
|
|
114
|
-
For each ARIA version (`1.1`, `1.2`, `1.3`), the
|
|
115
|
-
`props`, and `
|
|
114
|
+
For each ARIA version (`1.1`, `1.2`, `1.3`), the four arrays -- `roles`,
|
|
115
|
+
`props`, `graphicsRoles`, and `dpubRoles` -- are merged using `mergeArray`:
|
|
116
116
|
|
|
117
117
|
```ts
|
|
118
118
|
def['#aria'] = {
|
|
@@ -120,6 +120,7 @@ def['#aria'] = {
|
|
|
120
120
|
roles: mergeArray(def['#aria']['1.1'].roles, extendedSpec.def['#aria']['1.1'].roles),
|
|
121
121
|
props: mergeArray(def['#aria']['1.1'].props, extendedSpec.def['#aria']['1.1'].props),
|
|
122
122
|
graphicsRoles: mergeArray(def['#aria']['1.1'].graphicsRoles, extendedSpec.def['#aria']['1.1'].graphicsRoles),
|
|
123
|
+
dpubRoles: mergeArray(def['#aria']['1.1'].dpubRoles, extendedSpec.def['#aria']['1.1'].dpubRoles),
|
|
123
124
|
},
|
|
124
125
|
// same for 1.2 and 1.3
|
|
125
126
|
};
|
|
@@ -146,7 +147,36 @@ This means a framework can add its custom elements to existing categories
|
|
|
146
147
|
(e.g., adding `<router-link>` to `#phrasing`) or define entirely new
|
|
147
148
|
categories.
|
|
148
149
|
|
|
149
|
-
#### 5.
|
|
150
|
+
#### 5. Directive Patterns
|
|
151
|
+
|
|
152
|
+
If the extended spec provides `directivePatterns`, they are concatenated onto the
|
|
153
|
+
existing array:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
result.directivePatterns = [...(result.directivePatterns ?? []), ...extendedSpec.directivePatterns];
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
This is simple array concatenation. The core engine evaluates patterns in order
|
|
160
|
+
(first match wins), so framework specs should define their patterns from most
|
|
161
|
+
specific to most general.
|
|
162
|
+
|
|
163
|
+
#### 6. `useIDLAttributeNames`
|
|
164
|
+
|
|
165
|
+
If the extended spec explicitly sets `useIDLAttributeNames` (to `true` or
|
|
166
|
+
`false`), it overrides the current value:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
if (extendedSpec.useIDLAttributeNames != null) {
|
|
170
|
+
result.useIDLAttributeNames = extendedSpec.useIDLAttributeNames;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This is a last-write-wins semantic with explicit `null` guard -- omitting the
|
|
175
|
+
property does not reset a previously set value. This flag is consumed by
|
|
176
|
+
`@markuplint/ml-core`'s `MLAttr` constructor to enable IDL-to-content attribute
|
|
177
|
+
name resolution (e.g., `className` -> `class`).
|
|
178
|
+
|
|
179
|
+
#### 7. Element Specs
|
|
150
180
|
|
|
151
181
|
Elements are matched by name (case-insensitive comparison). For each element in
|
|
152
182
|
the base spec:
|
|
@@ -486,14 +516,14 @@ caches provide significant performance benefits.
|
|
|
486
516
|
|
|
487
517
|
### Cache Inventory
|
|
488
518
|
|
|
489
|
-
| # | Location | Cache Type | Key
|
|
490
|
-
| --- | ----------------------------------------------- | ----------------------------------------------------------------- |
|
|
491
|
-
| 1 | `getSpecByTagName` | `Map<string, any>` | Namespace-qualified name (e.g., `"svg:circle"`)
|
|
492
|
-
| 2 | `getVersionResolvedARIA` (inside `get-aria.ts`) | `Map<string, ARIA \| null>` | `localName + namespace + version` (string concatenation)
|
|
493
|
-
| 3 | `getContentModel` | `
|
|
494
|
-
| 4 | `contentModelCategoryToTagNames` | `Map<Category, ReadonlyArray<string>>` | Category string (e.g., `"#flow"`)
|
|
495
|
-
| 5 | `resolveNamespace` | `Map<string, NamespacedElementName>` | `name + namespaceURI` (string concatenation)
|
|
496
|
-
| 6 | `getAttrSpecs` (in `get-attr-specs-spec.ts`) | `Map<string, readonly Attribute[] \| null>` + `WeakSet<MLMLSpec>` | Namespace-qualified name
|
|
519
|
+
| # | Location | Cache Type | Key | Value | Invalidation |
|
|
520
|
+
| --- | ----------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------- |
|
|
521
|
+
| 1 | `getSpecByTagName` | `Map<string, any>` | Namespace-qualified name (e.g., `"svg:circle"`) | `ElementSpec \| null` | Module lifetime (never cleared) |
|
|
522
|
+
| 2 | `getVersionResolvedARIA` (inside `get-aria.ts`) | `Map<string, ARIA \| null>` | `localName + namespace + version` (string concatenation) | Version-resolved ARIA spec or null | Module lifetime (never cleared) |
|
|
523
|
+
| 3 | `getContentModel` | `WeakMap<Element, ...>` | DOM Element reference | `PermittedContentPattern[] \| boolean \| null` | Entries automatically removed when Element is GC'd |
|
|
524
|
+
| 4 | `contentModelCategoryToTagNames` | `Map<Category, ReadonlyArray<string>>` | Category string (e.g., `"#flow"`) | Frozen sorted array of tag names | Module lifetime (never cleared) |
|
|
525
|
+
| 5 | `resolveNamespace` | `Map<string, NamespacedElementName>` | `name + namespaceURI` (string concatenation) | Resolved namespace object | Module lifetime (never cleared) |
|
|
526
|
+
| 6 | `getAttrSpecs` (in `get-attr-specs-spec.ts`) | `Map<string, readonly Attribute[] \| null>` + `WeakSet<MLMLSpec>` | Namespace-qualified name | Sorted attribute array or null | Cleared when schema reference changes (via WeakSet check) |
|
|
497
527
|
|
|
498
528
|
### Cache Characteristics
|
|
499
529
|
|
|
@@ -516,11 +546,10 @@ if (!schemaCache.has(schema)) {
|
|
|
516
546
|
}
|
|
517
547
|
```
|
|
518
548
|
|
|
519
|
-
**
|
|
520
|
-
`
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
within the same spec context is O(1).
|
|
549
|
+
**WeakMap caching:** The `getContentModel` cache (item 3) uses a
|
|
550
|
+
`WeakMap<Element, ...>` keyed by DOM Element reference. Re-querying the same
|
|
551
|
+
element is O(1). When elements are garbage-collected (e.g., after a re-parse),
|
|
552
|
+
their cache entries are automatically removed, preventing memory leaks.
|
|
524
553
|
|
|
525
554
|
**Deterministic keys:** Caches in items 1, 2, and 5 use string concatenation
|
|
526
555
|
for keys. Since `resolveNamespace` produces deterministic output for the same
|
|
@@ -69,7 +69,7 @@ type SpecDefs = {
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
- **`#globalAttrs`** -- カテゴリをキーとした属性定義。各カテゴリ(例: `#HTMLGlobalAttrs`、`#ARIAAttrs`)は部分的な属性定義のレコードにマッピングされます。
|
|
72
|
-
- **`#aria`** -- 各仕様バージョン(1.1、1.2、1.3)の ARIA ロール/プロパティ定義。各バージョンには `roles`、`graphicsRoles`、`props` 配列が含まれます。
|
|
72
|
+
- **`#aria`** -- 各仕様バージョン(1.1、1.2、1.3)の ARIA ロール/プロパティ定義。各バージョンには `roles`、`graphicsRoles`、`dpubRoles`、`props` 配列が含まれます。
|
|
73
73
|
- **`#contentModels`** -- コンテンツモデルカテゴリからセレクタ配列へのマッピング。`Category` 値(`#flow`、`#phrasing` など)を、そのカテゴリに属する要素にマッチする CSS セレクタの配列にマッピングします。
|
|
74
74
|
|
|
75
75
|
### `ElementSpec`
|
|
@@ -137,9 +137,13 @@ type ARIARole = {
|
|
|
137
137
|
readonly isAbstract: boolean; // 抽象ロールかどうか
|
|
138
138
|
readonly deprecated: boolean; // 非推奨のロールかどうか
|
|
139
139
|
|
|
140
|
-
//
|
|
141
|
-
readonly
|
|
142
|
-
readonly
|
|
140
|
+
// コンテキスト要件(ARIA 1.3 名)
|
|
141
|
+
readonly requiredAccessibilityParentRole: readonly string[];
|
|
142
|
+
readonly allowedAccessibilityChildRoles: readonly string[];
|
|
143
|
+
|
|
144
|
+
// 後方互換性(ARIA 1.2 名、非推奨)
|
|
145
|
+
readonly requiredContextRole: readonly string[]; // @deprecated
|
|
146
|
+
readonly requiredOwnedElements: readonly string[]; // @deprecated
|
|
143
147
|
|
|
144
148
|
// アクセシブル名の制約
|
|
145
149
|
readonly accessibleNameRequired: boolean; // アクセシブル名が必須
|
package/docs/type-definitions.md
CHANGED
|
@@ -69,7 +69,7 @@ type SpecDefs = {
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
- **`#globalAttrs`** -- Category-keyed attribute definitions. Each category (e.g., `#HTMLGlobalAttrs`, `#ARIAAttrs`) maps to a record of partial attribute definitions.
|
|
72
|
-
- **`#aria`** -- ARIA role/property definitions for each specification version (1.1, 1.2, 1.3). Each version contains `roles`, `graphicsRoles`, and `props` arrays.
|
|
72
|
+
- **`#aria`** -- ARIA role/property definitions for each specification version (1.1, 1.2, 1.3). Each version contains `roles`, `graphicsRoles`, `dpubRoles`, and `props` arrays.
|
|
73
73
|
- **`#contentModels`** -- Content model category to selector array mappings. Maps `Category` values (like `#flow`, `#phrasing`) to arrays of CSS selectors that match elements belonging to that category.
|
|
74
74
|
|
|
75
75
|
### `ElementSpec`
|
|
@@ -137,9 +137,13 @@ type ARIARole = {
|
|
|
137
137
|
readonly isAbstract: boolean; // Whether this is an abstract role
|
|
138
138
|
readonly deprecated: boolean; // Whether this role is deprecated
|
|
139
139
|
|
|
140
|
-
// Context requirements
|
|
141
|
-
readonly
|
|
142
|
-
readonly
|
|
140
|
+
// Context requirements (ARIA 1.3 names)
|
|
141
|
+
readonly requiredAccessibilityParentRole: readonly string[];
|
|
142
|
+
readonly allowedAccessibilityChildRoles: readonly string[];
|
|
143
|
+
|
|
144
|
+
// Backward compatibility (ARIA 1.2 names, deprecated)
|
|
145
|
+
readonly requiredContextRole: readonly string[]; // @deprecated
|
|
146
|
+
readonly requiredOwnedElements: readonly string[]; // @deprecated
|
|
143
147
|
|
|
144
148
|
// Accessible name constraints
|
|
145
149
|
readonly accessibleNameRequired: boolean; // Must have an accessible name
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AccnameElement, AccnameNode, AccnameResolver } from '../types.js';
|
|
2
|
+
interface ElementOptions {
|
|
3
|
+
readonly attrs?: Record<string, string>;
|
|
4
|
+
readonly children?: readonly (AccnameElement | AccnameNode)[];
|
|
5
|
+
readonly parentElement?: AccnameElement | null;
|
|
6
|
+
readonly namespaceURI?: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates a test AccnameElement (plain object).
|
|
10
|
+
*
|
|
11
|
+
* **Note:** `textContent` is computed eagerly at creation time from the initial
|
|
12
|
+
* `children`. Adding children after creation will NOT update `textContent`.
|
|
13
|
+
* Build the full child tree before calling this function.
|
|
14
|
+
*
|
|
15
|
+
* @param localName - The local tag name of the element
|
|
16
|
+
* @param options - Configuration for attributes, children, parent, and namespace
|
|
17
|
+
* @returns A plain object implementing the AccnameElement interface
|
|
18
|
+
*/
|
|
19
|
+
export declare function element(localName: string, options?: ElementOptions): AccnameElement;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a test text node.
|
|
22
|
+
*
|
|
23
|
+
* @param text - The text content of the node
|
|
24
|
+
* @returns A plain object implementing the AccnameNode interface
|
|
25
|
+
*/
|
|
26
|
+
export declare function textNode(text: string): AccnameNode;
|
|
27
|
+
interface ResolverOptions {
|
|
28
|
+
readonly elements?: Map<string, AccnameElement>;
|
|
29
|
+
readonly labels?: Map<string, readonly AccnameElement[]>;
|
|
30
|
+
readonly nameFromContent?: Set<string>;
|
|
31
|
+
readonly hiddenIds?: Set<string>;
|
|
32
|
+
readonly allowsNameFromContentFn?: (el: AccnameElement) => boolean;
|
|
33
|
+
readonly isHiddenFn?: (el: AccnameElement) => boolean;
|
|
34
|
+
readonly embeddedControlRoles?: Set<string>;
|
|
35
|
+
readonly isEmbeddedControlFn?: (el: AccnameElement) => boolean;
|
|
36
|
+
readonly getPrecomputedNameFn?: (el: AccnameElement) => string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates a test AccnameResolver.
|
|
40
|
+
*
|
|
41
|
+
* @param options - Configuration for element lookups, label associations, and behavior overrides
|
|
42
|
+
* @returns A resolver implementing the AccnameResolver interface for testing
|
|
43
|
+
*/
|
|
44
|
+
export declare function createTestResolver(options?: ResolverOptions): AccnameResolver;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
|
+
import { ELEMENT_NODE, TEXT_NODE, XHTML_NAMESPACE, EMBEDDED_CONTROL_ROLES, isNativeEmbeddedControl, } from '../../../../const/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a test AccnameElement (plain object).
|
|
5
|
+
*
|
|
6
|
+
* **Note:** `textContent` is computed eagerly at creation time from the initial
|
|
7
|
+
* `children`. Adding children after creation will NOT update `textContent`.
|
|
8
|
+
* Build the full child tree before calling this function.
|
|
9
|
+
*
|
|
10
|
+
* @param localName - The local tag name of the element
|
|
11
|
+
* @param options - Configuration for attributes, children, parent, and namespace
|
|
12
|
+
* @returns A plain object implementing the AccnameElement interface
|
|
13
|
+
*/
|
|
14
|
+
export function element(localName, options = {}) {
|
|
15
|
+
const attrs = options.attrs ?? {};
|
|
16
|
+
const childNodes = options.children ?? [];
|
|
17
|
+
const elementChildren = [];
|
|
18
|
+
for (const child of childNodes) {
|
|
19
|
+
if (child.nodeType === ELEMENT_NODE) {
|
|
20
|
+
elementChildren.push(child);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const el = {
|
|
24
|
+
nodeType: ELEMENT_NODE,
|
|
25
|
+
localName,
|
|
26
|
+
id: attrs['id'] ?? '',
|
|
27
|
+
namespaceURI: options.namespaceURI ?? XHTML_NAMESPACE,
|
|
28
|
+
textContent: computeTextContent(childNodes),
|
|
29
|
+
parentElement: options.parentElement ?? null,
|
|
30
|
+
children: elementChildren,
|
|
31
|
+
childNodes,
|
|
32
|
+
getAttribute(name) {
|
|
33
|
+
return attrs[name] ?? null;
|
|
34
|
+
},
|
|
35
|
+
hasAttribute(name) {
|
|
36
|
+
return name in attrs;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
return el;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a test text node.
|
|
43
|
+
*
|
|
44
|
+
* @param text - The text content of the node
|
|
45
|
+
* @returns A plain object implementing the AccnameNode interface
|
|
46
|
+
*/
|
|
47
|
+
export function textNode(text) {
|
|
48
|
+
return {
|
|
49
|
+
nodeType: TEXT_NODE,
|
|
50
|
+
textContent: text,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function computeTextContent(childNodes) {
|
|
54
|
+
return childNodes
|
|
55
|
+
.map(child => {
|
|
56
|
+
if (child.nodeType === TEXT_NODE) {
|
|
57
|
+
return child.textContent ?? '';
|
|
58
|
+
}
|
|
59
|
+
if (child.nodeType === ELEMENT_NODE) {
|
|
60
|
+
return child.textContent ?? '';
|
|
61
|
+
}
|
|
62
|
+
return '';
|
|
63
|
+
})
|
|
64
|
+
.join('');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Creates a test AccnameResolver.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Configuration for element lookups, label associations, and behavior overrides
|
|
70
|
+
* @returns A resolver implementing the AccnameResolver interface for testing
|
|
71
|
+
*/
|
|
72
|
+
export function createTestResolver(options = {}) {
|
|
73
|
+
const elements = options.elements ?? new Map();
|
|
74
|
+
const labels = options.labels ?? new Map();
|
|
75
|
+
const nameFromContent = options.nameFromContent ?? new Set();
|
|
76
|
+
const hiddenIds = options.hiddenIds ?? new Set();
|
|
77
|
+
const embeddedControlRoles = options.embeddedControlRoles ?? EMBEDDED_CONTROL_ROLES;
|
|
78
|
+
return {
|
|
79
|
+
getElementById(id) {
|
|
80
|
+
return elements.get(id) ?? null;
|
|
81
|
+
},
|
|
82
|
+
getLabelsForId(id) {
|
|
83
|
+
return labels.get(id) ?? [];
|
|
84
|
+
},
|
|
85
|
+
allowsNameFromContent(el) {
|
|
86
|
+
if (options.allowsNameFromContentFn) {
|
|
87
|
+
return options.allowsNameFromContentFn(el);
|
|
88
|
+
}
|
|
89
|
+
return nameFromContent.has(el.localName);
|
|
90
|
+
},
|
|
91
|
+
isHidden(el) {
|
|
92
|
+
if (options.isHiddenFn) {
|
|
93
|
+
return options.isHiddenFn(el);
|
|
94
|
+
}
|
|
95
|
+
if (hiddenIds.has(el.id)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (el.getAttribute('aria-hidden') === 'true') {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (el.hasAttribute('hidden')) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
},
|
|
106
|
+
isEmbeddedControl(el) {
|
|
107
|
+
if (options.isEmbeddedControlFn) {
|
|
108
|
+
return options.isEmbeddedControlFn(el);
|
|
109
|
+
}
|
|
110
|
+
// Check explicit role
|
|
111
|
+
const role = el.getAttribute('role')?.trim().split(/\s+/)[0];
|
|
112
|
+
if (role && embeddedControlRoles.has(role)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// Check native HTML elements
|
|
116
|
+
return isNativeEmbeddedControl(el);
|
|
117
|
+
},
|
|
118
|
+
...(options.getPrecomputedNameFn ? { getPrecomputedName: options.getPrecomputedNameFn } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves accessible name via `aria-labelledby` attribute.
|
|
4
|
+
*
|
|
5
|
+
* Implements AccName 1.2 §4.3.2 Step 2B:
|
|
6
|
+
* "If the current node has an `aria-labelledby` attribute that contains
|
|
7
|
+
* at least one valid IDREF, and the current node is not already part of
|
|
8
|
+
* an ongoing `aria-labelledby` traversal, process its IDREFs [...]"
|
|
9
|
+
*
|
|
10
|
+
* Control flow:
|
|
11
|
+
* 1. Read `aria-labelledby` attribute; return null if absent or empty.
|
|
12
|
+
* 2. Split the attribute value by whitespace into IDREF tokens.
|
|
13
|
+
* 3. Mark the current element's ID as visited (cycle prevention for A→B→A).
|
|
14
|
+
* 4. For each IDREF:
|
|
15
|
+
* a. Skip if already visited — **except** self-references (see below).
|
|
16
|
+
* b. Resolve the referenced element via `resolver.getElementById`.
|
|
17
|
+
* c. Recursively compute the referenced element's name with
|
|
18
|
+
* `inLabelledbyTraversal=true` (prevents Step 2B re-entry in `compute.ts`).
|
|
19
|
+
* d. Each IDREF branch gets its own copy of the visited set so that
|
|
20
|
+
* one branch's traversal does not block later branches.
|
|
21
|
+
* 5. Join all resolved parts with a space separator and flatten whitespace.
|
|
22
|
+
*
|
|
23
|
+
* **Self-reference handling** (spec-defined, not a custom extension):
|
|
24
|
+
* An element may reference its own ID in `aria-labelledby` to include its
|
|
25
|
+
* own content alongside other referenced elements. This is explicitly
|
|
26
|
+
* permitted by the spec (AccName 1.2 §4.3.2 Example 2):
|
|
27
|
+
* `<h2 id="h" aria-labelledby="h foo">Meeting</h2>`
|
|
28
|
+
* Infinite recursion is prevented by `inLabelledbyTraversal`, not by the
|
|
29
|
+
* visited set — `computeFn` is called with `inLabelledbyTraversal=true`,
|
|
30
|
+
* which causes `compute.ts` to skip Step 2B on the referenced element.
|
|
31
|
+
*
|
|
32
|
+
* @param el - The element with a potential aria-labelledby attribute
|
|
33
|
+
* @param resolver - Environment-dependent resolver for element lookups
|
|
34
|
+
* @param visited - Set of element IDs already visited (cycle prevention)
|
|
35
|
+
* @param computeFn - The recursive accessible name computation function
|
|
36
|
+
* @returns The resolved name result, or null if aria-labelledby is not present or yields no name
|
|
37
|
+
* @see https://www.w3.org/TR/accname-1.2/#comp_labelledby_traversal — AccName 1.2 §4.3.2 Step 2B
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveAriaLabelledby(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult): AccnameResult | null;
|
|
40
|
+
/**
|
|
41
|
+
* Resolves accessible name via `aria-label` attribute.
|
|
42
|
+
*
|
|
43
|
+
* Implements AccName 1.2 §4.3.2 Step 2D:
|
|
44
|
+
* "If the current node has an `aria-label` attribute whose value is
|
|
45
|
+
* not undefined, not the empty string, and not a string of whitespace [...]"
|
|
46
|
+
*
|
|
47
|
+
* @param el - The element with a potential aria-label attribute
|
|
48
|
+
* @returns The resolved name result, or null if aria-label is not present or empty
|
|
49
|
+
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2D
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveAriaLabel(el: AccnameElement): AccnameResult | null;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
|
+
import { flattenText, makeResult } from './helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves accessible name via `aria-labelledby` attribute.
|
|
5
|
+
*
|
|
6
|
+
* Implements AccName 1.2 §4.3.2 Step 2B:
|
|
7
|
+
* "If the current node has an `aria-labelledby` attribute that contains
|
|
8
|
+
* at least one valid IDREF, and the current node is not already part of
|
|
9
|
+
* an ongoing `aria-labelledby` traversal, process its IDREFs [...]"
|
|
10
|
+
*
|
|
11
|
+
* Control flow:
|
|
12
|
+
* 1. Read `aria-labelledby` attribute; return null if absent or empty.
|
|
13
|
+
* 2. Split the attribute value by whitespace into IDREF tokens.
|
|
14
|
+
* 3. Mark the current element's ID as visited (cycle prevention for A→B→A).
|
|
15
|
+
* 4. For each IDREF:
|
|
16
|
+
* a. Skip if already visited — **except** self-references (see below).
|
|
17
|
+
* b. Resolve the referenced element via `resolver.getElementById`.
|
|
18
|
+
* c. Recursively compute the referenced element's name with
|
|
19
|
+
* `inLabelledbyTraversal=true` (prevents Step 2B re-entry in `compute.ts`).
|
|
20
|
+
* d. Each IDREF branch gets its own copy of the visited set so that
|
|
21
|
+
* one branch's traversal does not block later branches.
|
|
22
|
+
* 5. Join all resolved parts with a space separator and flatten whitespace.
|
|
23
|
+
*
|
|
24
|
+
* **Self-reference handling** (spec-defined, not a custom extension):
|
|
25
|
+
* An element may reference its own ID in `aria-labelledby` to include its
|
|
26
|
+
* own content alongside other referenced elements. This is explicitly
|
|
27
|
+
* permitted by the spec (AccName 1.2 §4.3.2 Example 2):
|
|
28
|
+
* `<h2 id="h" aria-labelledby="h foo">Meeting</h2>`
|
|
29
|
+
* Infinite recursion is prevented by `inLabelledbyTraversal`, not by the
|
|
30
|
+
* visited set — `computeFn` is called with `inLabelledbyTraversal=true`,
|
|
31
|
+
* which causes `compute.ts` to skip Step 2B on the referenced element.
|
|
32
|
+
*
|
|
33
|
+
* @param el - The element with a potential aria-labelledby attribute
|
|
34
|
+
* @param resolver - Environment-dependent resolver for element lookups
|
|
35
|
+
* @param visited - Set of element IDs already visited (cycle prevention)
|
|
36
|
+
* @param computeFn - The recursive accessible name computation function
|
|
37
|
+
* @returns The resolved name result, or null if aria-labelledby is not present or yields no name
|
|
38
|
+
* @see https://www.w3.org/TR/accname-1.2/#comp_labelledby_traversal — AccName 1.2 §4.3.2 Step 2B
|
|
39
|
+
*/
|
|
40
|
+
export function resolveAriaLabelledby(el, resolver, visited, computeFn) {
|
|
41
|
+
const labelledbyAttr = el.getAttribute('aria-labelledby');
|
|
42
|
+
if (!labelledbyAttr?.trim()) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const ids = labelledbyAttr.trim().split(/\s+/);
|
|
46
|
+
const parts = [];
|
|
47
|
+
const newVisited = new Set(visited);
|
|
48
|
+
// Mark the current element as visited to prevent other elements from
|
|
49
|
+
// circling back to it (e.g., A → B → A).
|
|
50
|
+
if (el.id) {
|
|
51
|
+
newVisited.add(el.id);
|
|
52
|
+
}
|
|
53
|
+
for (const id of ids) {
|
|
54
|
+
// Skip IDs already visited — but allow self-references (id === el.id).
|
|
55
|
+
// The spec requires that an element CAN reference itself in
|
|
56
|
+
// aria-labelledby to include its own content alongside other IDs.
|
|
57
|
+
// Example (AccName 1.2 §4.3.2 Example 2):
|
|
58
|
+
// <h2 id="h" aria-labelledby="h foo">Meeting</h2>
|
|
59
|
+
// Here "h" references itself, contributing "Meeting" to the result.
|
|
60
|
+
//
|
|
61
|
+
// Infinite recursion is prevented not by the visited set, but by
|
|
62
|
+
// inLabelledbyTraversal: computeFn is called with true below,
|
|
63
|
+
// which causes compute.ts to skip Step 2B (aria-labelledby) on
|
|
64
|
+
// the referenced element, so it never re-enters this function.
|
|
65
|
+
if (newVisited.has(id) && id !== el.id) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const referenced = resolver.getElementById(id);
|
|
69
|
+
if (!referenced) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Each branch gets its own visited set so that one IDREF's
|
|
73
|
+
// traversal doesn't block resolution of a later IDREF.
|
|
74
|
+
const innerVisited = new Set(newVisited);
|
|
75
|
+
innerVisited.add(id);
|
|
76
|
+
const result = computeFn(referenced, resolver, true, innerVisited);
|
|
77
|
+
if (result.name) {
|
|
78
|
+
parts.push(result.name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const name = flattenText(parts.join(' '));
|
|
82
|
+
if (name) {
|
|
83
|
+
return makeResult(name, 'aria-labelledby');
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolves accessible name via `aria-label` attribute.
|
|
89
|
+
*
|
|
90
|
+
* Implements AccName 1.2 §4.3.2 Step 2D:
|
|
91
|
+
* "If the current node has an `aria-label` attribute whose value is
|
|
92
|
+
* not undefined, not the empty string, and not a string of whitespace [...]"
|
|
93
|
+
*
|
|
94
|
+
* @param el - The element with a potential aria-label attribute
|
|
95
|
+
* @returns The resolved name result, or null if aria-label is not present or empty
|
|
96
|
+
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2D
|
|
97
|
+
*/
|
|
98
|
+
export function resolveAriaLabel(el) {
|
|
99
|
+
const label = el.getAttribute('aria-label');
|
|
100
|
+
if (label?.trim()) {
|
|
101
|
+
return makeResult(label, 'aria-label');
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes the accessible name for an element.
|
|
4
|
+
*
|
|
5
|
+
* Implements the Accessible Name and Description Computation algorithm
|
|
6
|
+
* per AccName 1.2 §4.3.2 ("Computation Steps") and HTML-AAM §4.1
|
|
7
|
+
* ("Accessible Name and Description Computation").
|
|
8
|
+
*
|
|
9
|
+
* Control flow (Steps map to AccName 1.2 §4.3.2):
|
|
10
|
+
*
|
|
11
|
+
* 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`) and NOT
|
|
12
|
+
* referenced by `aria-labelledby`, return empty immediately.
|
|
13
|
+
* 2. **Pre-computed name** — (Implementation-specific extension) If the
|
|
14
|
+
* resolver provides `getPrecomputedName`, check it before standard steps.
|
|
15
|
+
* Used by ml-core's Pretender integration for framework components.
|
|
16
|
+
* 3. **Step 2B — `aria-labelledby`** — Resolve referenced elements and
|
|
17
|
+
* recursively compute their names. Skipped when already inside a
|
|
18
|
+
* labelledby traversal (`inLabelledbyTraversal`) to prevent re-entry.
|
|
19
|
+
* 4. **Step 2D — `aria-label`** — Use the `aria-label` attribute value.
|
|
20
|
+
* 5. **Step 2E — Element-specific name** — Dispatch to HTML-AAM §4.1
|
|
21
|
+
* element-specific rules (label association, alt, value, legend, caption, SVG title).
|
|
22
|
+
* 6. **Step 2F — Name from content** — If the element's role allows
|
|
23
|
+
* `nameFrom: ["content"]`, or the element is referenced by `aria-labelledby`
|
|
24
|
+
* (`inLabelledbyTraversal`), recursively collect child text (including
|
|
25
|
+
* embedded control values per Step 2C).
|
|
26
|
+
* 7. **Step 2I — Title fallback** — Use the `title` attribute value.
|
|
27
|
+
*
|
|
28
|
+
* @param el - The element to compute the accessible name for
|
|
29
|
+
* @param resolver - Environment-dependent resolver for DOM traversal and role queries
|
|
30
|
+
* @returns The computed name and its source
|
|
31
|
+
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
|
|
32
|
+
* @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeAccessibleName(el: AccnameElement, resolver: AccnameResolver): AccnameResult;
|
|
35
|
+
/**
|
|
36
|
+
* Internal recursive entry point for accessible name computation.
|
|
37
|
+
*
|
|
38
|
+
* This function is the recursive core called by `resolveAriaLabelledby` (Step 2B),
|
|
39
|
+
* `resolveLabelText` (Step 2E), and `resolveNameFromContent` (Step 2F) when they
|
|
40
|
+
* need to compute a child or referenced element's name.
|
|
41
|
+
*
|
|
42
|
+
* @param el - The element to compute the accessible name for
|
|
43
|
+
* @param resolver - Environment-dependent resolver for DOM traversal and role queries
|
|
44
|
+
* @param inLabelledbyTraversal - When true, Step 2B is skipped to prevent re-entry
|
|
45
|
+
* into `resolveAriaLabelledby`, and Step 2F name-from-content is enabled regardless
|
|
46
|
+
* of role. This is set by Step 2B when processing each IDREF.
|
|
47
|
+
* @param visited - Set of element IDs already visited (cycle prevention for aria-labelledby)
|
|
48
|
+
* @returns The computed name and its source
|
|
49
|
+
* @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
|
|
50
|
+
*/
|
|
51
|
+
export declare function computeAccessibleNameInternal(el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>): AccnameResult;
|