@markuplint/ml-spec 5.0.0-rc.0 → 5.0.0-rc.2

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.
@@ -146,12 +146,12 @@ flowchart TD
146
146
 
147
147
  型システムは、マークアップ言語仕様・要素仕様・ARIA ロール・属性の構造を定義します。
148
148
 
149
- | ファイル | 役割 |
150
- | ------------------------------- | ----------------------------------------------------------------------------------------------------- |
151
- | `types/index.ts` | 手書き型: `MLMLSpec`, `ElementSpec`, `ExtendedSpec`, `ARIARole`, `ComputedRole` 等 |
152
- | `types/aria.ts` | 生成型: `ARIA`, `PermittedRoles`, `ImplicitRole`, `PermittedARIAProperties`, `ImplicitProperties` |
153
- | `types/attributes.ts` | 生成型: `AttributeType`, `GlobalAttributes`, `AttributeJSON`, `List`, `Enum`, `Number` |
154
- | `types/permitted-structures.ts` | 生成型: `PermittedContentPattern`, `ContentModel`, `Category`(HTML 13 + SVG 19 + MathML 3 カテゴリ) |
149
+ | ファイル | 役割 |
150
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
151
+ | `types/index.ts` | 手書き型: `MLMLSpec`, `ElementSpec`, `ExtendedSpec`, `ARIARole`, `ComputedRole` 等 |
152
+ | `types/aria.ts` | 生成型: `ARIA`, `PermittedRoles`, `ImplicitRole`, `PermittedARIAProperties`, `ImplicitProperties` |
153
+ | `types/attributes.ts` | 生成型: `AttributeType`, `ConditionalAttributeType`, `GlobalAttributes`, `AttributeJSON`, `List`, `Enum`, `Number`, `Directive` |
154
+ | `types/permitted-structures.ts` | 生成型: `PermittedContentPattern`, `ContentModel`, `Category`(HTML 13 + SVG 19 + MathML 3 カテゴリ) |
155
155
 
156
156
  ### 2. ARIA アルゴリズム
157
157
 
package/ARCHITECTURE.md CHANGED
@@ -146,12 +146,12 @@ flowchart TD
146
146
 
147
147
  The type system defines the structure of markup language specifications, element specs, ARIA roles, and attributes.
148
148
 
149
- | File | Purpose |
150
- | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
151
- | `types/index.ts` | Hand-written types: `MLMLSpec`, `ElementSpec`, `ExtendedSpec`, `ARIARole`, `ComputedRole`, etc. |
152
- | `types/aria.ts` | Generated: `ARIA`, `PermittedRoles`, `ImplicitRole`, `PermittedARIAProperties`, `ImplicitProperties` |
153
- | `types/attributes.ts` | Generated: `AttributeType`, `GlobalAttributes`, `AttributeJSON`, `List`, `Enum`, `Number` |
154
- | `types/permitted-structures.ts` | Generated: `PermittedContentPattern`, `ContentModel`, `Category` (HTML 13 + SVG 19 + MathML 3 categories) |
149
+ | File | Purpose |
150
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
151
+ | `types/index.ts` | Hand-written types: `MLMLSpec`, `ElementSpec`, `ExtendedSpec`, `ARIARole`, `ComputedRole`, etc. |
152
+ | `types/aria.ts` | Generated: `ARIA`, `PermittedRoles`, `ImplicitRole`, `PermittedARIAProperties`, `ImplicitProperties` |
153
+ | `types/attributes.ts` | Generated: `AttributeType`, `ConditionalAttributeType`, `GlobalAttributes`, `AttributeJSON`, `List`, `Enum`, `Number`, `Directive` |
154
+ | `types/permitted-structures.ts` | Generated: `PermittedContentPattern`, `ContentModel`, `Category` (HTML 13 + SVG 19 + MathML 3 categories) |
155
155
 
156
156
  ### 2. ARIA Algorithms
157
157
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
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-rc.2](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.1...v5.0.0-rc.2) (2026-04-15)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **ml-spec:** apply optimizePermittedRoles to condition-specific overrides ([f6ad782](https://github.com/markuplint/markuplint/commit/f6ad7823c298011fb690cd3360392bc2893c05f2)), closes [#3724](https://github.com/markuplint/markuplint/issues/3724)
11
+ - **ml-spec:** back-port content-model fields to schema source ([835d4d8](https://github.com/markuplint/markuplint/commit/835d4d8e4aa6bc2329b087194748159a8c1f86f6))
12
+ - **ml-spec:** treat permittedRoles:false as forbidding all explicit roles ([098cc41](https://github.com/markuplint/markuplint/commit/098cc4167297599e7721bf88119af7d8f3c2df08)), closes [#3641](https://github.com/markuplint/markuplint/issues/3641)
13
+
14
+ ### Features
15
+
16
+ - **html-spec:** add conditional value types for input element ([#3598](https://github.com/markuplint/markuplint/issues/3598)) ([290be1c](https://github.com/markuplint/markuplint/commit/290be1cde53165f1f6165731bf2ec184b0c46b54))
17
+ - **ml-spec:** add #nonEmptyText to Category type ([d60be3f](https://github.com/markuplint/markuplint/commit/d60be3f43bff6cc5d9c1cfa9dcdbe5d69f8697b8))
18
+ - **ml-spec:** add ConditionalAttributeType to Attribute.type union ([#3685](https://github.com/markuplint/markuplint/issues/3685)) ([a619a07](https://github.com/markuplint/markuplint/commit/a619a071d93566dd8aa7ab8dee2ed751c2e8756c)), closes [#3598](https://github.com/markuplint/markuplint/issues/3598) [#3189](https://github.com/markuplint/markuplint/issues/3189)
19
+ - **ml-spec:** add forbiddenAncestors field to ContentModel type ([5b32af5](https://github.com/markuplint/markuplint/commit/5b32af5ee7041843b63ca1542cb811d3192e8527))
20
+ - **ml-spec:** add uniqueAttrs field to ContentModel type ([d9e6f16](https://github.com/markuplint/markuplint/commit/d9e6f169888e92c9e8c85de884775cf026c2d1bf))
21
+
22
+ # [5.0.0-rc.1](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.0...v5.0.0-rc.1) (2026-03-27)
23
+
24
+ **Note:** Version bump only for package @markuplint/ml-spec
25
+
6
26
  # [5.0.0-rc.0](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.3...v5.0.0-rc.0) (2026-03-12)
7
27
 
8
28
  **Note:** Version bump only for package @markuplint/ml-spec
package/README.md CHANGED
@@ -96,7 +96,7 @@ Generated TypeScript types (do not edit directly):
96
96
 
97
97
  - **Built output**: `index.json` (48K+ lines, consolidated specification data)
98
98
  - **Sources**: `src/spec-*.json` (individual element specifications)
99
- - **Build process**: `build.mjs` → `@markuplint/spec-generator` → enriched with MDN/W3C data
99
+ - **Build process**: `build.ts` → `generator/` scripts → enriched with MDN/W3C data
100
100
 
101
101
  **@markuplint/ml-spec** (this package) provides:
102
102
 
@@ -240,16 +240,19 @@ if (result.role) {
240
240
  2. 仕様から `implicitRole` と `permittedRoles` を読み取ります。
241
241
  3. `permittedRoles` の値に基づいて許可ロールリストを構築します:
242
242
 
243
- | `permittedRoles` の値 | 動作 |
244
- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
245
- | `true` | ARIA 仕様のすべての非抽象ロール(DPub ロールを含む)が許可されます。 |
246
- | `PermittedARIAAAMInfo` オブジェクト | `core-aam` が `true` の場合、すべての非抽象ロールを追加。`graphics-aam` が `true` の場合、すべての非抽象グラフィックスロールを追加。 |
247
- | 文字列/オブジェクトの配列 | リストに記載された特定のロールが許可されます。 |
248
- | `false` | ロールは許可されません(暗黙のロール追加前は空リスト)。 |
249
-
250
- 4. 結果には常に暗黙のロールを含めます。暗黙のロールが `"presentation"` または `"none"` の場合、両方の等価ロールが含まれます。ARIA 1.3 では、暗黙のロールが `"img"` または `"image"` の場合、両方のシノニムが含まれます。
243
+ | `permittedRoles` の値 | 動作 |
244
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
245
+ | `true` | ARIA 仕様のすべての非抽象ロール(DPub ロールを含む)が許可されます。 |
246
+ | `PermittedARIAAAMInfo` オブジェクト | `core-aam` が `true` の場合、すべての非抽象ロールを追加。`graphics-aam` が `true` の場合、すべての非抽象グラフィックスロールを追加。 |
247
+ | 文字列/オブジェクトの配列 | リストに記載された特定のロールが許可されます。 |
248
+ | `false` | **明示的な `role` 属性値は一切許可されません** — 空リストを返し、暗黙ロールのマージ(手順4)をスキップします。ARIA in HTML の "No role permitted" 指示を厳密に反映する挙動です。 |
249
+
250
+ 4. `permittedRoles` `false` でない場合、結果に暗黙のロールを含めます。暗黙のロールが `"presentation"` または `"none"` の場合、両方の等価ロールが含まれます。ARIA 1.3 では、暗黙のロールが `"img"` または `"image"` の場合、両方のシノニムが含まれます。
251
251
  5. マージおよび重複排除されたリストを返します。
252
252
 
253
+ > [!NOTE]
254
+ > `permittedRoles: false` は厳密な挙動です。暗黙ロールに一致する値(例: `<img alt="" role="presentation">`)も拒否します。これは [ARIA in HTML](https://w3c.github.io/html-aria/) の「No role permitted」行が `role` 属性を一切許可しない仕様を反映したものです。背景は Issue #3641 を参照してください。
255
+
253
256
  ---
254
257
 
255
258
  ### 7. `getRoleSpec(specs, roleName, namespace, version)`
@@ -325,7 +328,6 @@ if (result.role) {
325
328
  1. `getVersionResolvedARIA()` を呼び出します。この関数は:
326
329
  - タグ名と名前空間で要素仕様を検索します。
327
330
  - `resolveVersion()` を適用して、ベース ARIA 仕様の上にバージョン固有のオーバーライドをマージします。
328
- - 許可ロールの最適化:許可ロール配列に `"presentation"` がある場合は `"none"` を追加し、逆も同様です(WAI-ARIA 1.2 の `none` ロールに関する注記に準拠)。ARIA 1.3 では、`"image"` がある場合は `"img"` を追加し、逆も同様です(ARIA 1.3 の `image`/`img` シノニムに準拠)。
329
331
  - 結果を `localName + namespace + version` をキーとしてキャッシュします。
330
332
 
331
333
  2. 条件付きオーバーライド(ARIA 仕様の `conditions` ブロック)を評価します:
@@ -333,7 +335,9 @@ if (result.role) {
333
335
  - マッチする条件ごとに、`implicitRole`、`permittedRoles`、`implicitProperties`、`properties`、`namingProhibited` を上書きします。
334
336
  - 後の条件は前の条件よりも優先されます。
335
337
 
336
- 3. 最終的に解決された ARIA 仕様を返します。要素の仕様が存在しない場合は `null` を返します。
338
+ 3. 許可ロールの最適化(ベース値と条件オーバーライド値の両方に適用):許可ロール配列に `"presentation"` がある場合は `"none"` を追加し、逆も同様です(WAI-ARIA 1.2 `none` ロールに関する注記に準拠)。ARIA 1.3 では、`"image"` がある場合は `"img"` を追加し、逆も同様です(ARIA 1.3 の `image`/`img` シノニムに準拠)。
339
+
340
+ 4. 最終的に解決された ARIA 仕様を返します。要素の仕様が存在しない場合は `null` を返します。
337
341
 
338
342
  **例:** `<input>` の場合、ベース仕様は汎用的な暗黙のロールを定義しますが、条件 `[type=checkbox]` がそれを `"checkbox"` ロールに上書きします。
339
343
 
@@ -240,16 +240,19 @@ The spec-level implementation for computing permitted ARIA roles. Operates on ta
240
240
  2. Reads `implicitRole` and `permittedRoles` from the spec.
241
241
  3. Builds the permitted role list based on the `permittedRoles` value:
242
242
 
243
- | `permittedRoles` value | Behavior |
244
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
245
- | `true` | All non-abstract roles from the ARIA spec (including DPub roles) are permitted. |
246
- | `PermittedARIAAAMInfo` object | If `core-aam` is `true`, adds all non-abstract roles. If `graphics-aam` is `true`, adds all non-abstract graphics roles. |
247
- | Array of strings/objects | The specific listed roles are permitted. |
248
- | `false` | No roles are permitted (empty list before implicit role). |
249
-
250
- 4. Always includes the implicit role in the result. If the implicit role is `"presentation"` or `"none"`, both equivalents are included. In ARIA 1.3, if the implicit role is `"img"` or `"image"`, both synonyms are included.
243
+ | `permittedRoles` value | Behavior |
244
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
245
+ | `true` | All non-abstract roles from the ARIA spec (including DPub roles) are permitted. |
246
+ | `PermittedARIAAAMInfo` object | If `core-aam` is `true`, adds all non-abstract roles. If `graphics-aam` is `true`, adds all non-abstract graphics roles. |
247
+ | Array of strings/objects | The specific listed roles are permitted. |
248
+ | `false` | **No explicit role is permitted** — returns an empty list and skips the implicit-role merge below. This models ARIA in HTML's "No role permitted" directive. |
249
+
250
+ 4. If `permittedRoles` was not `false`, includes the implicit role in the result. If the implicit role is `"presentation"` or `"none"`, both equivalents are included. In ARIA 1.3, if the implicit role is `"img"` or `"image"`, both synonyms are included.
251
251
  5. Returns the merged, deduplicated list.
252
252
 
253
+ > [!NOTE]
254
+ > `permittedRoles: false` is strict: even a value matching the implicit role (e.g. `<img alt="" role="presentation">`) is rejected. This mirrors the [ARIA in HTML](https://w3c.github.io/html-aria/) spec, where rows marked "No role permitted" forbid _any_ explicit `role` attribute. See Issue #3641 for background.
255
+
253
256
  ---
254
257
 
255
258
  ### 7. `getRoleSpec(specs, roleName, namespace, version)`
@@ -325,7 +328,6 @@ Gets the version-resolved ARIA specification for an element, evaluating conditio
325
328
  1. Calls `getVersionResolvedARIA()` which:
326
329
  - Looks up the element spec by tag name and namespace.
327
330
  - Applies `resolveVersion()` to merge version-specific overrides on top of the base ARIA spec.
328
- - Optimizes permitted roles: if `"presentation"` is in the permitted roles array, `"none"` is added, and vice versa (per WAI-ARIA 1.2 note on the `none` role). In ARIA 1.3, if `"image"` is present, `"img"` is added, and vice versa (per the ARIA 1.3 `image`/`img` synonym).
329
331
  - Caches results by `localName + namespace + version`.
330
332
 
331
333
  2. Evaluates conditional overrides (the `conditions` block in the ARIA spec):
@@ -333,7 +335,9 @@ Gets the version-resolved ARIA specification for an element, evaluating conditio
333
335
  - For each matching condition, overrides `implicitRole`, `permittedRoles`, `implicitProperties`, `properties`, and `namingProhibited`.
334
336
  - Later conditions take precedence over earlier ones.
335
337
 
336
- 3. Returns the final resolved ARIA spec, or `null` if no spec exists for the element.
338
+ 3. Optimizes permitted roles (applied to both base and condition-overridden values): if `"presentation"` is in the permitted roles array, `"none"` is added, and vice versa (per WAI-ARIA 1.2 note on the `none` role). In ARIA 1.3, if `"image"` is present, `"img"` is added, and vice versa (per the ARIA 1.3 `image`/`img` synonym).
339
+
340
+ 4. Returns the final resolved ARIA spec, or `null` if no spec exists for the element.
337
341
 
338
342
  **Example:** For `<input>`, the base spec may define a generic implicit role, but the condition `[type=checkbox]` overrides it to `"checkbox"`.
339
343
 
@@ -230,6 +230,34 @@ AttributeJSON: {
230
230
 
231
231
  再生成すると、新しいフィールドが `src/types/attributes.ts` にオプショナルプロパティとして反映されます。
232
232
 
233
+ > **警告:** `schemas/attributes.schema.json` と `schemas/global-attributes.schema.json` は `gen/gen.ts` によって _上書き_ されます。**これらのファイルを直接編集してはいけません** — 次に `schema:json`(または `yarn up:schema`)が走った瞬間に変更が消えます。すべての編集は `gen/gen.ts` に対して行ってください。
234
+
235
+ ### 3b. `attributes.schema.json` に新しい definition を追加する(新しいスキーマ型)
236
+
237
+ `AttributeJSON` のフィールドではなく、まったく新しい definition を追加したい場合 — 例えば #3685 の `ConditionalAttributeType` のような新しい構造化型バリアント — は、`gen/gen.ts` の 2 つめの `fs.writeFileSync` 呼び出し内の `definitions` オブジェクトに、`AttributeJSON` の隣に配置してください。
238
+
239
+ ```ts
240
+ fs.writeFileSync(
241
+ path.resolve(import.meta.dirname, '..', 'schemas', 'attributes.schema.json'),
242
+ JSON.stringify({
243
+ definitions: {
244
+ // ...既存の definitions...
245
+ NewType: {
246
+ type: 'object',
247
+ additionalProperties: false,
248
+ required: ['foo'],
249
+ properties: { foo: { type: 'string' } },
250
+ },
251
+ AttributeJSON: {
252
+ /* ... */
253
+ },
254
+ },
255
+ }),
256
+ );
257
+ ```
258
+
259
+ 再生成すると、新しい definition は TypeScript の `interface` として `src/types/attributes.ts` から export されます。下流パッケージ(例: `@markuplint/rules`)で narrowing が必要な場合は、`src/utils/` に型ガードを追加し `src/index.ts` から再 export してください — #3685 で確立したパターンは `is-conditional-attribute-type.ts` を参照してください。
260
+
233
261
  ### 4. ARIA ロール/プロパティスキーマを変更する
234
262
 
235
263
  `schemas/aria.schema.json` を直接編集します。例えば、ロール定義に新しいフィールドを追加する場合:
@@ -230,6 +230,34 @@ AttributeJSON: {
230
230
 
231
231
  Then regenerate. The new field will appear in `src/types/attributes.ts` as an optional property.
232
232
 
233
+ > **Warning:** `schemas/attributes.schema.json` and `schemas/global-attributes.schema.json` are both _overwritten_ by `gen/gen.ts`. **Never hand-edit those files** — your changes will be blown away the next time `schema:json` (or `yarn up:schema`) runs. All edits must live in `gen/gen.ts`.
234
+
235
+ ### 3b. Add a new definition to `attributes.schema.json` (new schema type)
236
+
237
+ When you need to add a brand-new definition (not just a field on `AttributeJSON`) — for example, adding a new structured type variant like `ConditionalAttributeType` (#3685) — edit `gen/gen.ts` and place the new definition alongside `AttributeJSON` inside the `definitions` object of the second `fs.writeFileSync` call.
238
+
239
+ ```ts
240
+ fs.writeFileSync(
241
+ path.resolve(import.meta.dirname, '..', 'schemas', 'attributes.schema.json'),
242
+ JSON.stringify({
243
+ definitions: {
244
+ // ...existing definitions...
245
+ NewType: {
246
+ type: 'object',
247
+ additionalProperties: false,
248
+ required: ['foo'],
249
+ properties: { foo: { type: 'string' } },
250
+ },
251
+ AttributeJSON: {
252
+ /* ... */
253
+ },
254
+ },
255
+ }),
256
+ );
257
+ ```
258
+
259
+ After regeneration, the new definition is exported from `src/types/attributes.ts` as a TypeScript `interface`. If downstream packages (e.g. `@markuplint/rules`) need to narrow against it, add a type guard in `src/utils/` and re-export from `src/index.ts` — see `is-conditional-attribute-type.ts` for the pattern established by #3685.
260
+
233
261
  ### 4. Modify ARIA role/property schema
234
262
 
235
263
  Edit `schemas/aria.schema.json` directly. For example, to add a new field to the role definition:
@@ -113,7 +113,10 @@ type ElementSpec = {
113
113
  ```ts
114
114
  type Attribute = {
115
115
  readonly name: string;
116
- readonly type: ReadonlyDeep<AttributeType> | readonly ReadonlyDeep<AttributeType>[];
116
+ readonly type:
117
+ | ReadonlyDeep<AttributeType>
118
+ | readonly ReadonlyDeep<AttributeType>[]
119
+ | readonly ReadonlyDeep<ConditionalAttributeType>[];
117
120
  readonly description?: string;
118
121
  readonly caseSensitive?: true;
119
122
  readonly experimental?: boolean;
@@ -125,6 +128,8 @@ type Attribute = {
125
128
 
126
129
  `& ExtendableAttributeSpec` の交差型により、生成された `AttributeJSON` 型のフィールド(`type` を除く。`ReadonlyDeep` ラッパーでオーバーライドされるため)が追加されます: `defaultValue`、`required`、`requiredEither`、`noUse`、`condition`、`ineffective`、`animatable`。
127
130
 
131
+ `type` の共用体には、別の属性の値によって値型が切り替わる属性のために `readonly ReadonlyDeep<ConditionalAttributeType>[]` が含まれます(下記 [`ConditionalAttributeType`](#attributesschemajson-から----srctypesattributests) を参照)。このバリアントは v5.0 では型定義のみで先行投入されており(#3685)、実ロジックは follow-up Issue #3598 / #3189 で実装されます。
132
+
128
133
  ## ARIA 型
129
134
 
130
135
  ### `ARIARole`
@@ -419,7 +424,10 @@ interface GlobalAttributes {
419
424
 
420
425
  ```ts
421
426
  interface AttributeJSON {
422
- type?: AttributeType | [AttributeType, ...AttributeType[]];
427
+ type?:
428
+ | AttributeType
429
+ | [AttributeType, ...AttributeType[]]
430
+ | [ConditionalAttributeType, ...ConditionalAttributeType[]];
423
431
  defaultValue?: string;
424
432
  deprecated?: boolean;
425
433
  required?: boolean | AttributeCondition;
@@ -432,6 +440,17 @@ interface AttributeJSON {
432
440
  }
433
441
  ```
434
442
 
443
+ **`ConditionalAttributeType`** -- 条件付き型切り替え: ある属性の期待する値型が、同じ要素上の _別の_ 属性の値によって切り替わることを宣言します。例えば `<input value>` は `type=color` のとき valid simple color、`type=url` のとき valid URL でなければならない、あるいは `<link as>` は `rel` が `preload` か `modulepreload` かによって異なる destination キーワードを受け入れる、といった仕様表現に使われます。
444
+
445
+ ```ts
446
+ interface ConditionalAttributeType {
447
+ condition: AttributeCondition; // 所属要素に対する CSS セレクター
448
+ type: AttributeType | [AttributeType, ...AttributeType[]];
449
+ }
450
+ ```
451
+
452
+ > **ステータス:** 型定義は v5.0 で投入済み (#3685)。条件解決は完全に実装済み: `type` による `input[value]` は [#3598](https://github.com/markuplint/markuplint/issues/3598)、`rel` による `link[as]` は [#3189](https://github.com/markuplint/markuplint/issues/3189)。`helpers.ts` の `isValidAttr()` が要素を各条件にマッチさせ、解決された型でバリデーションを行います。条件に一致しない場合は `Any` にフォールバックします。直接呼び出し元向けに、`attrCheck()` に安全ネットガードが残っており、未解決の `ConditionalAttributeType[]` は有効として扱います。
453
+
435
454
  **`List`** -- スペース区切りまたはカンマ区切りのトークンリスト用の構造化型です。
436
455
 
437
456
  ```ts
@@ -113,7 +113,10 @@ Describes a single HTML/SVG attribute with its type, description, and status fla
113
113
  ```ts
114
114
  type Attribute = {
115
115
  readonly name: string;
116
- readonly type: ReadonlyDeep<AttributeType> | readonly ReadonlyDeep<AttributeType>[];
116
+ readonly type:
117
+ | ReadonlyDeep<AttributeType>
118
+ | readonly ReadonlyDeep<AttributeType>[]
119
+ | readonly ReadonlyDeep<ConditionalAttributeType>[];
117
120
  readonly description?: string;
118
121
  readonly caseSensitive?: true;
119
122
  readonly experimental?: boolean;
@@ -125,6 +128,8 @@ type Attribute = {
125
128
 
126
129
  The `& ExtendableAttributeSpec` intersection adds fields from the generated `AttributeJSON` type (excluding `type`, which is overridden with the `ReadonlyDeep` wrapper): `defaultValue`, `required`, `requiredEither`, `noUse`, `condition`, `ineffective`, `animatable`.
127
130
 
131
+ The `type` union includes `readonly ReadonlyDeep<ConditionalAttributeType>[]` for attributes whose value type depends on another attribute's value (see [`ConditionalAttributeType`](#from-attributesschemajson----srctypesattributests) below). This variant is shipped type-only in v5.0 (#3685); follow-ups #3598 and #3189 implement the runtime resolution.
132
+
128
133
  ## ARIA Types
129
134
 
130
135
  ### `ARIARole`
@@ -419,7 +424,10 @@ interface GlobalAttributes {
419
424
 
420
425
  ```ts
421
426
  interface AttributeJSON {
422
- type?: AttributeType | [AttributeType, ...AttributeType[]];
427
+ type?:
428
+ | AttributeType
429
+ | [AttributeType, ...AttributeType[]]
430
+ | [ConditionalAttributeType, ...ConditionalAttributeType[]];
423
431
  defaultValue?: string;
424
432
  deprecated?: boolean;
425
433
  required?: boolean | AttributeCondition;
@@ -432,6 +440,17 @@ interface AttributeJSON {
432
440
  }
433
441
  ```
434
442
 
443
+ **`ConditionalAttributeType`** -- Conditional type switching: declares that an attribute's expected value type depends on the value of _another_ attribute on the same element. Used when the spec states, for example, that `<input value>` must be a valid simple color when `type=color` but a valid URL when `type=url`, or that `<link as>` accepts different destination keywords depending on whether `rel` is `preload` or `modulepreload`.
444
+
445
+ ```ts
446
+ interface ConditionalAttributeType {
447
+ condition: AttributeCondition; // CSS selector against the owning element
448
+ type: AttributeType | [AttributeType, ...AttributeType[]];
449
+ }
450
+ ```
451
+
452
+ > **Status:** The type definition shipped in v5.0 (#3685). Conditional resolution is fully implemented: `input[value]` by `type` in [#3598](https://github.com/markuplint/markuplint/issues/3598), `link[as]` by `rel` in [#3189](https://github.com/markuplint/markuplint/issues/3189). `isValidAttr()` in `helpers.ts` matches the element against each condition and validates against the resolved type; unmatched conditions fall back to `Any`. A safety-net guard in `attrCheck()` treats any remaining unresolved `ConditionalAttributeType[]` as valid for direct callers.
453
+
435
454
  **`List`** -- Structured type for space-separated or comma-separated token lists.
436
455
 
437
456
  ```ts
@@ -71,8 +71,7 @@ export function resolveAriaLabelledby(el, resolver, visited, computeFn) {
71
71
  }
72
72
  // Each branch gets its own visited set so that one IDREF's
73
73
  // traversal doesn't block resolution of a later IDREF.
74
- const innerVisited = new Set(newVisited);
75
- innerVisited.add(id);
74
+ const innerVisited = new Set([...newVisited, id]);
76
75
  const result = computeFn(referenced, resolver, true, innerVisited);
77
76
  if (result.name) {
78
77
  parts.push(result.name);
@@ -20,7 +20,10 @@ export function getARIA(specs, localName, namespace, version, matches) {
20
20
  }
21
21
  const conditions = aria.conditions;
22
22
  if (!conditions) {
23
- return aria;
23
+ return {
24
+ ...aria,
25
+ permittedRoles: optimizePermittedRoles(aria.permittedRoles, version),
26
+ };
24
27
  }
25
28
  const conditionKeys = Object.keys(conditions);
26
29
  let { implicitRole, permittedRoles, implicitProperties, properties, namingProhibited } = aria;
@@ -56,12 +59,16 @@ export function getARIA(specs, localName, namespace, version, matches) {
56
59
  }
57
60
  return {
58
61
  implicitRole,
59
- permittedRoles,
62
+ permittedRoles: optimizePermittedRoles(permittedRoles, version),
60
63
  implicitProperties,
61
64
  properties,
62
65
  namingProhibited,
63
66
  };
64
67
  }
68
+ /**
69
+ * Returns raw ARIA spec without permittedRoles optimization.
70
+ * Callers must apply optimizePermittedRoles() to the result.
71
+ */
65
72
  function getVersionResolvedARIA(specs, localName, namespace, version) {
66
73
  const key = localName + namespace + version;
67
74
  let aria = cache.get(key);
@@ -74,12 +81,6 @@ function getVersionResolvedARIA(specs, localName, namespace, version) {
74
81
  return null;
75
82
  }
76
83
  aria = resolveVersion(spec, version);
77
- if (aria.permittedRoles !== false) {
78
- aria = {
79
- ...aria,
80
- permittedRoles: optimizePermittedRoles(aria.permittedRoles, version),
81
- };
82
- }
83
84
  cache.set(key, aria);
84
85
  return aria;
85
86
  }
@@ -60,6 +60,13 @@ export function getPermittedRoles(specs, localName, namespace, version, matches)
60
60
  return role;
61
61
  }));
62
62
  }
63
+ // When `permittedRoles` is explicitly `false`, no explicit role attribute is
64
+ // allowed at all per ARIA in HTML — even a value matching the implicit role.
65
+ // e.g. `<img alt="">`: implicit role is `presentation`, but "No role permitted"
66
+ // means `role="presentation"` and `role="none"` are both disallowed.
67
+ if (permittedRoles === false) {
68
+ return permittedRoleList;
69
+ }
63
70
  if (implicitRole === false) {
64
71
  return permittedRoleList;
65
72
  }
package/lib/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export * from './utils/schema-to-spec.js';
27
27
  export * from './utils/resolve-namespace.js';
28
28
  export * from './utils/validate-aria-version.js';
29
29
  export * from './utils/directive-resolver.js';
30
+ export * from './utils/is-conditional-attribute-type.js';
30
31
  export * from './types/index.js';
31
32
  export * from './types/aria.js';
32
33
  export * from './types/attributes.js';
package/lib/index.js CHANGED
@@ -31,6 +31,7 @@ export * from './utils/schema-to-spec.js';
31
31
  export * from './utils/resolve-namespace.js';
32
32
  export * from './utils/validate-aria-version.js';
33
33
  export * from './utils/directive-resolver.js';
34
+ export * from './utils/is-conditional-attribute-type.js';
34
35
  // Type definitions
35
36
  export * from './types/index.js';
36
37
  export * from './types/aria.js';
@@ -43,25 +43,26 @@ export type PermittedARIAProperties = false | {
43
43
  */
44
44
  without?: [
45
45
  {
46
- type: 'not-recommended' | 'should-not' | 'must-not';
46
+ type: "not-recommended" | "should-not" | "must-not";
47
47
  name: string;
48
48
  value?: string;
49
49
  alt?: {
50
- method: 'remove-attr' | 'set-attr';
50
+ method: "remove-attr" | "set-attr";
51
51
  target: string;
52
52
  };
53
53
  },
54
54
  ...{
55
- type: 'not-recommended' | 'should-not' | 'must-not';
55
+ type: "not-recommended" | "should-not" | "must-not";
56
56
  name: string;
57
57
  value?: string;
58
58
  alt?: {
59
- method: 'remove-attr' | 'set-attr';
59
+ method: "remove-attr" | "set-attr";
60
60
  target: string;
61
61
  };
62
62
  }[]
63
63
  ];
64
64
  };
65
+ export type ConditionsOrFalse = false | Conditions;
65
66
  export interface AriaSchema {
66
67
  _?: ARIA;
67
68
  [k: string]: unknown;
@@ -72,81 +73,34 @@ export interface ARIA {
72
73
  namingProhibited?: true;
73
74
  implicitProperties?: ImplicitProperties;
74
75
  properties?: PermittedARIAProperties;
75
- conditions?: {
76
- /**
77
- * This interface was referenced by `undefined`'s JSON-Schema definition
78
- * via the `patternProperty` ".+".
79
- */
80
- [k: string]: {
81
- implicitRole?: ImplicitRole;
82
- permittedRoles?: PermittedRoles;
83
- namingProhibited?: true;
84
- implicitProperties?: ImplicitProperties;
85
- properties?: PermittedARIAProperties;
86
- };
87
- };
88
- '1.3'?: {
76
+ conditions?: Conditions;
77
+ "1.3"?: {
89
78
  implicitRole?: ImplicitRole;
90
79
  permittedRoles?: PermittedRoles;
91
80
  namingProhibited?: true;
92
81
  implicitProperties?: ImplicitProperties;
93
82
  properties?: PermittedARIAProperties;
94
- conditions?: false | {
95
- /**
96
- * This interface was referenced by `undefined`'s JSON-Schema definition
97
- * via the `patternProperty` ".+".
98
- */
99
- [k: string]: {
100
- implicitRole?: ImplicitRole;
101
- permittedRoles?: PermittedRoles;
102
- namingProhibited?: true;
103
- implicitProperties?: ImplicitProperties;
104
- properties?: PermittedARIAProperties;
105
- };
106
- };
83
+ conditions?: ConditionsOrFalse;
107
84
  };
108
- '1.2'?: {
85
+ "1.2"?: {
109
86
  implicitRole?: ImplicitRole;
110
87
  permittedRoles?: PermittedRoles;
111
88
  namingProhibited?: true;
112
89
  implicitProperties?: ImplicitProperties;
113
90
  properties?: PermittedARIAProperties;
114
- conditions?: false | {
115
- /**
116
- * This interface was referenced by `undefined`'s JSON-Schema definition
117
- * via the `patternProperty` ".+".
118
- */
119
- [k: string]: {
120
- implicitRole?: ImplicitRole;
121
- permittedRoles?: PermittedRoles;
122
- namingProhibited?: true;
123
- implicitProperties?: ImplicitProperties;
124
- properties?: PermittedARIAProperties;
125
- };
126
- };
91
+ conditions?: ConditionsOrFalse;
127
92
  };
128
- '1.1'?: {
93
+ "1.1"?: {
129
94
  implicitRole?: ImplicitRole;
130
95
  permittedRoles?: PermittedRoles;
131
96
  implicitProperties?: ImplicitProperties;
132
97
  properties?: PermittedARIAProperties;
133
- conditions?: false | {
134
- /**
135
- * This interface was referenced by `undefined`'s JSON-Schema definition
136
- * via the `patternProperty` ".+".
137
- */
138
- [k: string]: {
139
- implicitRole?: ImplicitRole;
140
- permittedRoles?: PermittedRoles;
141
- implicitProperties?: ImplicitProperties;
142
- properties?: PermittedARIAProperties;
143
- };
144
- };
98
+ conditions?: ConditionsOrFalse;
145
99
  };
146
100
  }
147
101
  export interface PermittedARIAAAMInfo {
148
- 'core-aam'?: true;
149
- 'graphics-aam'?: true;
102
+ "core-aam"?: true;
103
+ "graphics-aam"?: true;
150
104
  }
151
105
  export interface ImplicitProperties {
152
106
  /**
@@ -155,3 +109,16 @@ export interface ImplicitProperties {
155
109
  */
156
110
  [k: string]: string;
157
111
  }
112
+ export interface Conditions {
113
+ /**
114
+ * This interface was referenced by `Conditions`'s JSON-Schema definition
115
+ * via the `patternProperty` ".+".
116
+ */
117
+ [k: string]: {
118
+ implicitRole?: ImplicitRole;
119
+ permittedRoles?: PermittedRoles;
120
+ namingProhibited?: true;
121
+ implicitProperties?: ImplicitProperties;
122
+ properties?: PermittedARIAProperties;
123
+ };
124
+ }
package/lib/types/aria.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * This file was automatically generated by json-schema-to-typescript.
3
4
  * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,