@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.
Files changed (75) hide show
  1. package/ARCHITECTURE.ja.md +24 -10
  2. package/ARCHITECTURE.md +24 -10
  3. package/CHANGELOG.md +18 -0
  4. package/README.md +2 -2
  5. package/docs/aria-algorithms.ja.md +180 -33
  6. package/docs/aria-algorithms.md +183 -34
  7. package/docs/html-algorithms.ja.md +2 -2
  8. package/docs/html-algorithms.md +2 -2
  9. package/docs/maintenance.ja.md +13 -22
  10. package/docs/maintenance.md +13 -22
  11. package/docs/spec-resolution.ja.md +37 -9
  12. package/docs/spec-resolution.md +45 -16
  13. package/docs/type-definitions.ja.md +8 -4
  14. package/docs/type-definitions.md +8 -4
  15. package/lib/algorithm/aria/accname/__tests__/test-helpers.d.ts +45 -0
  16. package/lib/algorithm/aria/accname/__tests__/test-helpers.js +120 -0
  17. package/lib/algorithm/aria/accname/aria-steps.d.ts +51 -0
  18. package/lib/algorithm/aria/accname/aria-steps.js +104 -0
  19. package/lib/algorithm/aria/accname/compute.d.ts +51 -0
  20. package/lib/algorithm/aria/accname/compute.js +101 -0
  21. package/lib/algorithm/aria/accname/element-names.d.ts +36 -0
  22. package/lib/algorithm/aria/accname/element-names.js +342 -0
  23. package/lib/algorithm/aria/accname/helpers.d.ts +98 -0
  24. package/lib/algorithm/aria/accname/helpers.js +330 -0
  25. package/lib/algorithm/aria/accname/index.d.ts +4 -0
  26. package/lib/algorithm/aria/accname/index.js +3 -0
  27. package/lib/algorithm/aria/accname/label-steps.d.ts +25 -0
  28. package/lib/algorithm/aria/accname/label-steps.js +66 -0
  29. package/lib/algorithm/aria/accname/svg-helpers.d.ts +17 -0
  30. package/lib/algorithm/aria/accname/svg-helpers.js +30 -0
  31. package/lib/algorithm/aria/accname/types.d.ts +70 -0
  32. package/lib/algorithm/aria/accname/types.js +2 -0
  33. package/lib/algorithm/aria/accname-computation.d.ts +14 -3
  34. package/lib/algorithm/aria/accname-computation.js +131 -8
  35. package/lib/algorithm/aria/aria-specs.d.ts +1 -0
  36. package/lib/algorithm/aria/get-aria.js +30 -4
  37. package/lib/algorithm/aria/get-computed-role.js +106 -26
  38. package/lib/algorithm/aria/get-explicit-role.d.ts +12 -0
  39. package/lib/algorithm/aria/get-explicit-role.js +12 -0
  40. package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +12 -0
  41. package/lib/algorithm/aria/get-non-presentational-ancestor.js +14 -2
  42. package/lib/algorithm/aria/get-permitted-roles-spec.d.ts +8 -4
  43. package/lib/algorithm/aria/get-permitted-roles-spec.js +19 -6
  44. package/lib/algorithm/aria/get-role-spec.js +10 -3
  45. package/lib/algorithm/aria/has-required-owned-elements.d.ts +2 -1
  46. package/lib/algorithm/aria/has-required-owned-elements.js +18 -15
  47. package/lib/algorithm/aria/is-presentational.d.ts +24 -0
  48. package/lib/algorithm/aria/is-presentational.js +31 -0
  49. package/lib/algorithm/aria/matches-context-role.d.ts +12 -4
  50. package/lib/algorithm/aria/matches-context-role.js +39 -6
  51. package/lib/algorithm/html/content-model-category-to-tag-names.js +1 -1
  52. package/lib/algorithm/html/get-content-model.d.ts +4 -2
  53. package/lib/algorithm/html/get-content-model.js +6 -7
  54. package/lib/const/accname.d.ts +29 -0
  55. package/lib/const/accname.js +76 -0
  56. package/lib/const/dom.d.ts +8 -0
  57. package/lib/const/dom.js +8 -0
  58. package/lib/const/index.d.ts +2 -0
  59. package/lib/const/index.js +2 -0
  60. package/lib/index.d.ts +3 -0
  61. package/lib/index.js +4 -0
  62. package/lib/types/index.d.ts +65 -0
  63. package/lib/utils/directive-resolver.d.ts +23 -0
  64. package/lib/utils/directive-resolver.js +50 -0
  65. package/lib/utils/get-attr-specs-spec.js +4 -3
  66. package/lib/utils/get-ns.d.ts +7 -0
  67. package/lib/utils/get-ns.js +7 -0
  68. package/lib/utils/get-spec-by-tag-name.d.ts +1 -1
  69. package/lib/utils/merge-array.d.ts +10 -0
  70. package/lib/utils/merge-array.js +10 -0
  71. package/lib/utils/resolve-version.d.ts +11 -0
  72. package/lib/utils/resolve-version.js +11 -0
  73. package/lib/utils/schema-to-spec.d.ts +2 -0
  74. package/lib/utils/schema-to-spec.js +15 -4
  75. 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` の3つの配列が `mergeArray` を使ってマージされます:
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` | `Map<Specs, Map<Element, ...>>` | 外側: specs配列参照; 内側: DOM Element参照 | `PermittedContentPattern[] \| boolean \| null` | specs参照ごとに外側マップエントリを作成 |
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
- **ネストされたキャッシュ:** `getContentModel` キャッシュ(項目3)は
508
- 2段階の `Map<Specs, Map<Element, ...>>` 構造を使用します。
509
- 外側のマップはspecs配列参照でキーが付けられるため、
510
- 異なる仕様設定は別々のキャッシュを維持します。
511
- 内側のマップはDOM Element参照でキーが付けられるため、
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` は同じ入力に対して決定論的な出力を生成し、
@@ -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 three arrays -- `roles`,
115
- `props`, and `graphicsRoles` -- are merged using `mergeArray`:
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. Element Specs
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 | Value | Invalidation |
490
- | --- | ----------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------- |
491
- | 1 | `getSpecByTagName` | `Map<string, any>` | Namespace-qualified name (e.g., `"svg:circle"`) | `ElementSpec \| null` | Module lifetime (never cleared) |
492
- | 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) |
493
- | 3 | `getContentModel` | `Map<Specs, Map<Element, ...>>` | Outer: specs array reference; Inner: DOM Element reference | `PermittedContentPattern[] \| boolean \| null` | Outer map entry created per unique specs reference |
494
- | 4 | `contentModelCategoryToTagNames` | `Map<Category, ReadonlyArray<string>>` | Category string (e.g., `"#flow"`) | Frozen sorted array of tag names | Module lifetime (never cleared) |
495
- | 5 | `resolveNamespace` | `Map<string, NamespacedElementName>` | `name + namespaceURI` (string concatenation) | Resolved namespace object | Module lifetime (never cleared) |
496
- | 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) |
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
- **Nested caching:** The `getContentModel` cache (item 3) uses a two-level
520
- `Map<Specs, Map<Element, ...>>` structure. The outer map is keyed by the specs
521
- array reference, so different spec configurations maintain separate caches. The
522
- inner map is keyed by DOM Element reference, so re-querying the same element
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 requiredContextRole: readonly string[]; // 必要な親ロール
142
- readonly requiredOwnedElements: readonly string[]; // 必要な子ロール
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; // アクセシブル名が必須
@@ -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 requiredContextRole: readonly string[]; // Required parent roles
142
- readonly requiredOwnedElements: readonly string[]; // Required child roles
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;