@markuplint/ml-core 4.13.3 → 5.0.0-alpha.1

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 (57) hide show
  1. package/ARCHITECTURE.ja.md +92 -35
  2. package/ARCHITECTURE.md +85 -28
  3. package/CHANGELOG.md +54 -0
  4. package/docs/linting-pipeline.ja.md +18 -14
  5. package/docs/linting-pipeline.md +18 -14
  6. package/docs/maintenance.ja.md +1 -1
  7. package/docs/maintenance.md +1 -1
  8. package/docs/ml-dom/attr.ja.md +8 -0
  9. package/docs/ml-dom/attr.md +8 -0
  10. package/docs/ml-dom/block.ja.md +33 -33
  11. package/docs/ml-dom/block.md +33 -33
  12. package/docs/ml-dom/element.ja.md +17 -17
  13. package/docs/ml-dom/element.md +17 -17
  14. package/docs/ml-dom/node.ja.md +4 -5
  15. package/docs/ml-dom/node.md +4 -5
  16. package/docs/ml-dom/others.ja.md +2 -1
  17. package/docs/ml-dom/others.md +5 -4
  18. package/docs/rule-system.ja.md +23 -6
  19. package/docs/rule-system.md +23 -6
  20. package/lib/index.d.ts +4 -3
  21. package/lib/index.js +1 -1
  22. package/lib/ml-core.d.ts +1 -1
  23. package/lib/ml-core.js +142 -82
  24. package/lib/ml-dom/helper/accname.d.ts +8 -0
  25. package/lib/ml-dom/helper/accname.js +71 -55
  26. package/lib/ml-dom/helper/create-node.js +1 -0
  27. package/lib/ml-dom/helper/get-indent.js +17 -29
  28. package/lib/ml-dom/node/attr.js +122 -73
  29. package/lib/ml-dom/node/block.d.ts +3 -3
  30. package/lib/ml-dom/node/block.js +10 -1
  31. package/lib/ml-dom/node/document-type.js +12 -0
  32. package/lib/ml-dom/node/document.d.ts +14 -3
  33. package/lib/ml-dom/node/document.js +75 -34
  34. package/lib/ml-dom/node/dom-token-list.js +17 -30
  35. package/lib/ml-dom/node/element-close-tag.js +1 -0
  36. package/lib/ml-dom/node/element.d.ts +19 -7
  37. package/lib/ml-dom/node/element.js +134 -55
  38. package/lib/ml-dom/node/node-store.js +6 -15
  39. package/lib/ml-dom/node/node.d.ts +3 -1
  40. package/lib/ml-dom/node/node.js +159 -166
  41. package/lib/ml-dom/node/parent-node.js +14 -30
  42. package/lib/ml-dom/node/rule-mapper.js +7 -20
  43. package/lib/ml-dom/node/text.d.ts +7 -0
  44. package/lib/ml-dom/node/text.js +9 -0
  45. package/lib/ml-dom/token/token.js +23 -39
  46. package/lib/ml-rule/create-rule.d.ts +8 -1
  47. package/lib/ml-rule/create-rule.js +0 -9
  48. package/lib/ml-rule/ml-rule-context.js +7 -11
  49. package/lib/ml-rule/ml-rule.d.ts +33 -1
  50. package/lib/ml-rule/ml-rule.js +65 -25
  51. package/lib/ruleset/index.js +6 -0
  52. package/lib/test/index.js +4 -1
  53. package/lib/types.d.ts +2 -1
  54. package/lib/violation-collector.js +15 -28
  55. package/lib/virtual-rule.d.ts +72 -0
  56. package/lib/virtual-rule.js +233 -0
  57. package/package.json +16 -13
@@ -44,6 +44,8 @@ src/
44
44
  │ └── manipulations/
45
45
  │ ├── child-node-methods.ts — ChildNode インターフェーススタブ
46
46
  │ └── get-children.ts — 要素の子要素抽出
47
+ ├── virtual-rule.ts — Named nodeRule の展開(expandNamedNodeRules)
48
+ ├── virtual-rule.spec.ts — 仮想ルールのユニットテスト
47
49
  ├── ml-rule/
48
50
  │ ├── ml-rule.ts — MLRule クラス(ルール実行)
49
51
  │ ├── ml-rule-context.ts — MLRuleContext(レポート収集)
@@ -173,7 +175,7 @@ MLToken<A extends MLASTToken>
173
175
  | `MLText` | `Text` | テキストノード、`isWhitespace()`, `isRawTextElementContent()` |
174
176
  | `MLComment` | `Comment` | コメントノード(`textContent`) |
175
177
  | `MLDocumentType` | `DocumentType` | `<!DOCTYPE>`(`name`, `publicId`, `systemId`) |
176
- | `MLBlock` | — | プリプロセッサ固有ブロック(if/each/switch)、`conditionalType`, `isTransparent` |
178
+ | `MLBlock` | — | プリプロセッサ固有ブロック(if/each/switch)、`blockBehavior`, `isTransparent` |
177
179
  | `MLElementCloseTag` | — | 開始タグ要素とペアになる閉じタグ |
178
180
  | `MLParentNode` | `ParentNode` | `querySelector()`, `querySelectorAll()`, `children`, `childElementCount` |
179
181
  | `MLElement` | `Element` | 属性、セレクタ、名前空間、pretender コンテキスト、`elementType`, `closeTag` |
@@ -204,14 +206,14 @@ MLToken<A extends MLASTToken>
204
206
 
205
207
  ### 主要メソッド
206
208
 
207
- | メソッド | 説明 |
208
- | --------------------------------- | --------------------------------------------------------------------------------------------- |
209
- | `walkOn(type, walker)` | 指定した型(`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`)のノードを走査 |
210
- | `setRule(rule)` | 現在のルールを設定(検証時に `MLCore` が使用) |
211
- | `getTokenList()` | ソース再構築用の全トークンを返す |
212
- | `searchNodeByLocation(line, col)` | 指定したソース位置のノードを検索 |
213
- | `getAccessibilityProp(node)` | ARIA アクセシビリティプロパティを計算 |
214
- | `toString(fixed?)` | ソースコードを再構築(オプションで修正適用) |
209
+ | メソッド | 説明 |
210
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
211
+ | `walkOn(type, walker)` | 指定した型(`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`)のノードを走査 |
212
+ | `setRule(rule)` | 現在のルールを設定(検証時に `MLCore` が使用) |
213
+ | `getTokenList()` | ソース再構築用の全トークンを返す |
214
+ | `searchNodeByLocation(line, col)` | 指定したソース位置のノードを検索 |
215
+ | `getAccessibilityProp(node)` | ARIA アクセシビリティプロパティを計算(`MLElement.getAccessibleName()` のキャッシュ経由でアクセシブルネームを取得) |
216
+ | `toString(fixed?)` | ソースコードを再構築(オプションで修正適用) |
215
217
 
216
218
  ## MLElement
217
219
 
@@ -232,15 +234,16 @@ MLToken<A extends MLASTToken>
232
234
 
233
235
  ### 主要メソッド
234
236
 
235
- | メソッド | 説明 |
236
- | ---------------------------- | -------------------------------------------------------------- |
237
- | `getAttribute(name)` | 属性値または `null` を返す |
238
- | `getAttributeToken(name)` | 名前付き属性の `MLAttr[]` を返す |
239
- | `hasAttribute(name)` | 属性の存在を確認 |
240
- | `matches(selector)` | CSS セレクタマッチング |
241
- | `matchMLSelector(selector)` | 拡張 markuplint セレクタマッチング(`RegexSelector` サポート) |
242
- | `querySelector(selector)` | 最初にマッチする子孫を検索 |
243
- | `querySelectorAll(selector)` | マッチするすべての子孫を検索 |
237
+ | メソッド | 説明 |
238
+ | ---------------------------- | ------------------------------------------------------------------- |
239
+ | `getAttribute(name)` | 属性値または `null` を返す |
240
+ | `getAttributeToken(name)` | 名前付き属性の `MLAttr[]` を返す |
241
+ | `hasAttribute(name)` | 属性の存在を確認 |
242
+ | `getAccessibleName(version)` | キャッシュ付きアクセシブルネーム計算(ARIA バージョンごとにメモ化) |
243
+ | `matches(selector)` | CSS セレクタマッチング |
244
+ | `matchMLSelector(selector)` | 拡張 markuplint セレクタマッチング(`RegexSelector` サポート) |
245
+ | `querySelector(selector)` | 最初にマッチする子孫を検索 |
246
+ | `querySelectorAll(selector)` | マッチするすべての子孫を検索 |
244
247
 
245
248
  ## ルールシステム
246
249
 
@@ -248,13 +251,17 @@ MLToken<A extends MLASTToken>
248
251
 
249
252
  `MLRule<T, O>` はリンティングルールを検証およびオプションの修正ロジックとともにカプセル化します。
250
253
 
251
- | プロパティ/メソッド | 説明 |
252
- | --------------------------------- | ---------------------------------------- |
253
- | `name` | ルール識別子(例:`"attr-duplication"`) |
254
- | `defaultSeverity` | デフォルトの重大度レベル |
255
- | `defaultValue` / `defaultOptions` | デフォルト設定 |
256
- | `verify(document, locale, fix)` | ルールを実行して違反を返す |
257
- | `optimizeOption(settings)` | 生のルール設定を `RuleInfo` に正規化 |
254
+ | プロパティ/メソッド | 説明 |
255
+ | --------------------------------- | ------------------------------------------------------------------------------- |
256
+ | `name` | ルール識別子(例:`"attr-duplication"`) |
257
+ | `defaultSeverity` | デフォルトの重大度レベル |
258
+ | `defaultValue` / `defaultOptions` | デフォルト設定 |
259
+ | `baseRuleId` | 仮想ルールの場合: ベースルール名(例:`"required-attr"`) |
260
+ | `groupName` | 複数エントリ仮想ルールの場合: 一括無効化用のグループ名 |
261
+ | `specConformance` | 仮想ルールの場合: `'normative'` または `'non-normative'`(named nodeRule 由来) |
262
+ | `verify(document, locale, fix)` | ルールを実行して違反を返す |
263
+ | `createAlias(name, options?)` | このルールの verify/fix ロジックを再利用する仮想ルールを作成 |
264
+ | `optimizeOption(settings)` | 生のルール設定を `RuleInfo` に正規化 |
258
265
 
259
266
  ### RuleSeed
260
267
 
@@ -310,6 +317,56 @@ flowchart TD
310
317
  G --> H["Violation[] を返す"]
311
318
  ```
312
319
 
320
+ ### 仮想ルールシステム
321
+
322
+ ソース: `src/virtual-rule.ts`
323
+
324
+ > **用語ポリシー**: 「仮想ルール (virtual rule)」は**コントリビューター向けの内部実装用語**です。ユーザー向けドキュメント(ウェブサイト、移行ガイド、README)では**「named rule」**を使用すること。設定ユーザーの視点では、**ベースルール**(例: `required-attr`)と **named rule**(例: `a11y/html-lang`)の2つの概念だけで十分です。`MLRule` のエイリアス機構という内部メカニズムを公開してはなりません。
325
+
326
+ 仮想ルールは、**名前付き nodeRules** — `/` を含む `name` プロパティを持つ nodeRule エントリ(例: `"a11y/html-lang"`)— から作成される独立した `MLRule` インスタンスです。これによりチェック単位の制御が可能になります: 各仮想ルールは `rules["alias/name"]: false` で個別に有効/無効化できます。
327
+
328
+ #### Named NodeRule の展開
329
+
330
+ `expandNamedNodeRules()` は `MLCore` の構築時に named nodeRules(および childNodeRules)を仮想ルールに変換します:
331
+
332
+ ```
333
+ Named nodeRule(設定) 仮想 MLRule(ランタイム)
334
+ ┌─────────────────────────┐ ┌──────────────────────────┐
335
+ │ name: "a11y/html-lang" │ │ name: "a11y/html-lang" │
336
+ │ specConformance: "norm."│ ──────► │ baseRuleId: "required-attr" │
337
+ │ selector: ":where(html)"│ │ specConformance: メタデータ│
338
+ │ rules: │ │ verify/fix: ベースから │
339
+ │ required-attr: [lang] │ └──────────────────────────┘
340
+ └─────────────────────────┘
341
+ ```
342
+
343
+ 主要な動作:
344
+
345
+ - **false エントリの分離**: `rules` 内の `false` エントリは自動的に無名 nodeRule に分離され、ベースルールの specificity override としてのセマンティクスが維持される
346
+ - **複数エントリサポート**: 非 false エントリが 2 つ以上の named nodeRule は派生名(`name/baseRuleName`)と `groupName` で作成され、グループ一括無効化が可能
347
+ - **メタデータ**: `specConformance` は下流ツールやレポート向けのメタデータとして仮想ルールに付与される
348
+ - **ホットリロード**: 展開前の nodeRules は `#originalNodeRules` / `#originalChildNodeRules` に保持され、`update()` で再展開可能
349
+
350
+ #### なぜ `specConformance` は Named NodeRule 専用なのか
351
+
352
+ `specConformance` は意図的に **named nodeRule(プリセット内)でのみ**使用可能であり、通常の組み込みルールでは使用できません。設計根拠は以下の通りです:
353
+
354
+ 1. **組み込みルールは既に正しいデフォルト重大度を持っている。** `permitted-contents` や `required-attr` のようなルールは本質的に normative(WHATWG の MUST 要件を強制する)であり、`defaultSeverity` は既に `'error'` に設定されています。別途 `specConformance` フラグは不要です — 重大度は組み込み済みです。
355
+
356
+ 2. **Named nodeRules はプリセット作成者による仕様解釈である。** `preset.html-standard.jsonc` のようなプリセットが `"html-standard/head-charset-utf8"` という named nodeRule を作成する場合、プリセット作成者は特定の仕様要件をチェックとして表現しています。`specConformance` により、その要件の RFC 2119 キーワード強度を宣言でき、下流のツールやレポートが違反の仕様由来の分類を識別できます。
357
+
358
+ 3. **ユーザーは自分のルールに `specConformance` を設定すべきではない。** カスタムコンポーネント(例: `<MyComponent>` の props 検証)に対するユーザー定義の nodeRule は仕様準拠チェックではなく、プロジェクトの規約です。任意のユーザー設定で `specConformance` を許可すると、「HTML 仕様がこれを要求している」と「チームがこれを好む」の区別が曖昧になります。`name` プロパティ(`/` を含む必要あり)がゲートキーパーとして機能します:named nodeRule のみが `specConformance` を持てる設計であり、named nodeRule は仕様を理解するプリセット作成者向けに設計されています。
359
+
360
+ まとめ: `specConformance` は仕様由来のチェックを識別するメタデータを提供する**プリセットレベルのアノテーション**です。組み込みルールは `defaultSeverity` で独自に重大度を管理します。ユーザー定義ルールはルール設定の `severity` フィールドで直接重大度を表現します。
361
+
362
+ #### 仮想ルールの無効化
363
+
364
+ 仮想ルールは `rules` 設定で3つのレベルで無効化できます:
365
+
366
+ 1. **完全一致**: `rules["a11y/html-lang"]: false`
367
+ 2. **グループ無効化**: `rules["custom/multi"]: false`(複数エントリの named nodeRule 用)
368
+ 3. **名前空間ワイルドカード**: `rules["a11y/*"]: false`(`a11y/` で始まるすべての仮想ルールを無効化)
369
+
313
370
  ## Pretender システム
314
371
 
315
372
  pretender システムにより、コンポーネントをリンティング時にセマンティック HTML 要素として扱うことができます。これにより、ルールがカスタムコンポーネント(例:`<MyButton>`)を標準要素(例:`<button>`)として検証できます。
@@ -338,16 +395,16 @@ type Pretender = {
338
395
 
339
396
  テンプレートエンジン(Pug, EJS, Nunjucks など)はプリプロセッサ固有のブロックを生成し、`MLBlock` ノードで表現されます。これらのブロックは子ノードを条件付きでラップできます:
340
397
 
341
- | `conditionalType` | テンプレート構文 | 説明 |
342
- | ----------------- | ----------------- | ------------------ |
343
- | `'if:start'` | `{% if %}` | 条件ブロックの開始 |
344
- | `'if:else'` | `{% else %}` | 代替分岐 |
345
- | `'if:end'` | `{% endif %}` | 条件ブロックの終了 |
346
- | `'each:start'` | `{% for %}` | ループの開始 |
347
- | `'each:end'` | `{% endfor %}` | ループの終了 |
348
- | `'switch:start'` | `{% switch %}` | switch の開始 |
349
- | `'switch:case'` | `{% case %}` | switch ケース |
350
- | `'switch:end'` | `{% endswitch %}` | switch の終了 |
398
+ | `blockBehavior.type` | テンプレート構文 | 説明 |
399
+ | -------------------- | ----------------- | ------------------ |
400
+ | `'if'` | `{% if %}` | 条件ブロックの開始 |
401
+ | `'if:else'` | `{% else %}` | 代替分岐 |
402
+ | `'end'` | `{% endif %}` | 条件ブロックの終了 |
403
+ | `'each'` | `{% for %}` | ループの開始 |
404
+ | `'end'` | `{% endfor %}` | ループの終了 |
405
+ | `'switch:case'` | `{% switch %}` | switch の開始 |
406
+ | `'switch:default'` | `{% case %}` | switch ケース |
407
+ | `'end'` | `{% endswitch %}` | switch の終了 |
351
408
 
352
409
  `MLNode.conditionalChildNodes()` は `NodeListOf` 配列の配列を返します(条件分岐ごとに 1 つ)。これにより、ルールは各分岐を独立して分析できます。
353
410
 
package/ARCHITECTURE.md CHANGED
@@ -44,6 +44,8 @@ src/
44
44
  │ └── manipulations/
45
45
  │ ├── child-node-methods.ts — ChildNode interface stubs
46
46
  │ └── get-children.ts — Element children extraction
47
+ ├── virtual-rule.ts — Named nodeRule expansion (expandNamedNodeRules)
48
+ ├── virtual-rule.spec.ts — Virtual rule unit tests
47
49
  ├── ml-rule/
48
50
  │ ├── ml-rule.ts — MLRule class (rule execution)
49
51
  │ ├── ml-rule-context.ts — MLRuleContext (report collection)
@@ -173,7 +175,7 @@ MLToken<A extends MLASTToken>
173
175
  | `MLText` | `Text` | Text nodes, `isWhitespace()`, `isRawTextElementContent()` |
174
176
  | `MLComment` | `Comment` | Comment nodes with `textContent` |
175
177
  | `MLDocumentType` | `DocumentType` | `<!DOCTYPE>` with `name`, `publicId`, `systemId` |
176
- | `MLBlock` | — | Preprocessor-specific blocks (if/each/switch), `conditionalType`, `isTransparent` |
178
+ | `MLBlock` | — | Preprocessor-specific blocks (if/each/switch), `blockBehavior`, `isTransparent` |
177
179
  | `MLElementCloseTag` | — | Close tag paired with its open tag element |
178
180
  | `MLParentNode` | `ParentNode` | `querySelector()`, `querySelectorAll()`, `children`, `childElementCount` |
179
181
  | `MLElement` | `Element` | Attributes, selectors, namespaces, pretender context, `elementType`, `closeTag` |
@@ -204,14 +206,14 @@ The constructor receives an `MLASTDocument`, a `Ruleset`, and an `MLSchema` tupl
204
206
 
205
207
  ### Key Methods
206
208
 
207
- | Method | Description |
208
- | --------------------------------- | ----------------------------------------------------------------------------------------------- |
209
- | `walkOn(type, walker)` | Walks nodes of a given type (`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`) |
210
- | `setRule(rule)` | Sets the current rule, used by `MLCore` during verification |
211
- | `getTokenList()` | Returns all tokens for source reconstruction |
212
- | `searchNodeByLocation(line, col)` | Finds the node at a given source position |
213
- | `getAccessibilityProp(node)` | Computes ARIA accessibility properties |
214
- | `toString(fixed?)` | Reconstructs source code, optionally with fixes applied |
209
+ | Method | Description |
210
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
211
+ | `walkOn(type, walker)` | Walks nodes of a given type (`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`) |
212
+ | `setRule(rule)` | Sets the current rule, used by `MLCore` during verification |
213
+ | `getTokenList()` | Returns all tokens for source reconstruction |
214
+ | `searchNodeByLocation(line, col)` | Finds the node at a given source position |
215
+ | `getAccessibilityProp(node)` | Computes ARIA accessibility properties (delegates to `MLElement.getAccessibleName()` for cached accessible name) |
216
+ | `toString(fixed?)` | Reconstructs source code, optionally with fixes applied |
215
217
 
216
218
  ## MLElement
217
219
 
@@ -237,6 +239,7 @@ The constructor receives an `MLASTDocument`, a `Ruleset`, and an `MLSchema` tupl
237
239
  | `getAttribute(name)` | Returns attribute value or `null` |
238
240
  | `getAttributeToken(name)` | Returns `MLAttr[]` for the named attribute |
239
241
  | `hasAttribute(name)` | Checks attribute existence |
242
+ | `getAccessibleName(version)` | Cached accessible name computation (memoized per ARIA version) |
240
243
  | `matches(selector)` | CSS selector matching |
241
244
  | `matchMLSelector(selector)` | Extended markuplint selector matching (supports `RegexSelector`) |
242
245
  | `querySelector(selector)` | Finds first matching descendant |
@@ -248,13 +251,17 @@ The constructor receives an `MLASTDocument`, a `Ruleset`, and an `MLSchema` tupl
248
251
 
249
252
  `MLRule<T, O>` encapsulates a linting rule with verification and optional fix logic.
250
253
 
251
- | Property/Method | Description |
252
- | --------------------------------- | ------------------------------------------------- |
253
- | `name` | Rule identifier (e.g., `"attr-duplication"`) |
254
- | `defaultSeverity` | Default severity level |
255
- | `defaultValue` / `defaultOptions` | Default configuration |
256
- | `verify(document, locale, fix)` | Executes the rule and returns violations |
257
- | `optimizeOption(settings)` | Normalizes raw rule configuration into `RuleInfo` |
254
+ | Property/Method | Description |
255
+ | --------------------------------- | --------------------------------------------------------------------------- |
256
+ | `name` | Rule identifier (e.g., `"attr-duplication"`) |
257
+ | `defaultSeverity` | Default severity level |
258
+ | `defaultValue` / `defaultOptions` | Default configuration |
259
+ | `baseRuleId` | For virtual rules: the base rule's name (e.g., `"required-attr"`) |
260
+ | `groupName` | For multi-entry virtual rules: group name for batch disable |
261
+ | `specConformance` | For virtual rules: `'normative'` or `'non-normative'` (from named nodeRule) |
262
+ | `verify(document, locale, fix)` | Executes the rule and returns violations |
263
+ | `createAlias(name, options?)` | Creates a virtual rule that reuses this rule's verify/fix logic |
264
+ | `optimizeOption(settings)` | Normalizes raw rule configuration into `RuleInfo` |
258
265
 
259
266
  ### RuleSeed
260
267
 
@@ -310,6 +317,56 @@ flowchart TD
310
317
  G --> H["Return Violation[]"]
311
318
  ```
312
319
 
320
+ ### Virtual Rule System
321
+
322
+ Source: `src/virtual-rule.ts`
323
+
324
+ > **Terminology policy**: "Virtual rule" is an **internal implementation term** for contributors only. User-facing documentation (website, migration guides, README) must use **"named rule"** instead. From a config user's perspective, there are only two concepts: a **base rule** (e.g., `required-attr`) and a **named rule** (e.g., `a11y/html-lang`). The internal mechanics of `MLRule` aliasing should not be exposed.
325
+
326
+ Virtual rules are independent `MLRule` instances created from **named nodeRules** — nodeRule entries with a `name` property containing `/` (e.g., `"a11y/html-lang"`). This enables per-check control: each virtual rule can be independently enabled/disabled via `rules["alias/name"]: false`.
327
+
328
+ #### Named NodeRule Expansion
329
+
330
+ `expandNamedNodeRules()` converts named nodeRules (and childNodeRules) into virtual rules during `MLCore` construction:
331
+
332
+ ```
333
+ Named nodeRule (config) Virtual MLRule (runtime)
334
+ ┌─────────────────────────┐ ┌──────────────────────────┐
335
+ │ name: "a11y/html-lang" │ │ name: "a11y/html-lang" │
336
+ │ specConformance: "norm."│ ──────► │ baseRuleId: "required-attr" │
337
+ │ selector: ":where(html)"│ │ specConformance: metadata│
338
+ │ rules: │ │ verify/fix: from base │
339
+ │ required-attr: [lang] │ └──────────────────────────┘
340
+ └─────────────────────────┘
341
+ ```
342
+
343
+ Key behaviors:
344
+
345
+ - **False entry separation**: `false` entries in `rules` are automatically separated into unnamed nodeRules, preserving their semantics as base-rule specificity overrides
346
+ - **Multi-entry support**: Named nodeRules with 2+ non-false entries create derived names (`name/baseRuleName`) with a `groupName` for group disable
347
+ - **Metadata**: `specConformance` is attached to the virtual rule as metadata for downstream tools and reporting
348
+ - **Hot-reload**: Pre-expansion nodeRules are preserved in `#originalNodeRules` / `#originalChildNodeRules` so `update()` can re-expand them
349
+
350
+ #### Why `specConformance` Is Restricted to Named NodeRules
351
+
352
+ `specConformance` is intentionally available **only on named nodeRules** (in presets), not on regular built-in rules. The design rationale:
353
+
354
+ 1. **Built-in rules already have correct default severity.** Rules like `permitted-contents` or `required-attr` are inherently normative (they enforce WHATWG MUST requirements), and their `defaultSeverity` is already set to `'error'`. There is no need for a separate `specConformance` flag — the severity is baked in.
355
+
356
+ 2. **Named nodeRules are preset-authored spec interpretations.** When a preset like `preset.html-standard.jsonc` creates a named nodeRule `"html-standard/head-charset-utf8"`, the preset author is expressing a specific spec requirement as a check. `specConformance` lets the author declare the RFC 2119 keyword strength of that requirement, so downstream tools and reports can identify which violations originate from spec requirements and at what normative level.
357
+
358
+ 3. **Users should not set `specConformance` on their own rules.** A user-defined nodeRule for a custom component (e.g., validating `<MyComponent>` props) is not a spec conformance check — it is a project convention. Allowing `specConformance` on arbitrary user config would blur the distinction between "the HTML spec requires this" and "our team prefers this". The `name` property (which requires `/`) serves as a gatekeeper: only named nodeRules can carry `specConformance`, and named nodeRules are designed for preset authors who understand the spec.
359
+
360
+ In summary: `specConformance` is a **preset-level annotation** that provides metadata about which spec requirements a check enforces. Built-in rules handle their own severity via `defaultSeverity`. User-defined rules express severity directly via the `severity` field in rule config.
361
+
362
+ #### Virtual Rule Disable
363
+
364
+ Virtual rules can be disabled at three levels in the `rules` config:
365
+
366
+ 1. **Exact name**: `rules["a11y/html-lang"]: false`
367
+ 2. **Group disable**: `rules["custom/multi"]: false` (for multi-entry named nodeRules)
368
+ 3. **Namespace wildcard**: `rules["a11y/*"]: false` (disables all virtual rules starting with `a11y/`)
369
+
313
370
  ## Pretender System
314
371
 
315
372
  The pretender system allows components to be treated as semantic HTML elements during linting. This enables rules to validate custom components (e.g., `<MyButton>`) as if they were standard elements (e.g., `<button>`).
@@ -338,18 +395,18 @@ type Pretender = {
338
395
 
339
396
  Template engines (Pug, EJS, Nunjucks, etc.) produce preprocessor-specific blocks represented by `MLBlock` nodes. These blocks can wrap child nodes conditionally:
340
397
 
341
- | `conditionalType` | Template Construct | Description |
342
- | ----------------- | ------------------ | -------------------------- |
343
- | `'if:start'` | `{% if %}` | Start of conditional block |
344
- | `'if:else'` | `{% else %}` | Alternative branch |
345
- | `'if:end'` | `{% endif %}` | End of conditional block |
346
- | `'each:start'` | `{% for %}` | Start of loop |
347
- | `'each:end'` | `{% endfor %}` | End of loop |
348
- | `'switch:start'` | `{% switch %}` | Start of switch |
349
- | `'switch:case'` | `{% case %}` | Switch case |
350
- | `'switch:end'` | `{% endswitch %}` | End of switch |
351
-
352
- `MLNode.conditionalChildNodes()` returns an array of `NodeListOf` arrays — one per conditional branch — so rules can analyze each branch independently.
398
+ | `blockBehavior.type` | Template Construct | Description |
399
+ | -------------------- | ------------------ | -------------------------- |
400
+ | `'if'` | `{% if %}` | Start of conditional block |
401
+ | `'if:else'` | `{% else %}` | Alternative branch |
402
+ | `'end'` | `{% endif %}` | End of conditional block |
403
+ | `'each'` | `{% for %}` | Start of loop |
404
+ | `'end'` | `{% endfor %}` | End of loop |
405
+ | `'switch:case'` | `{% switch %}` | Start of switch |
406
+ | `'switch:default'` | `{% case %}` | Switch case |
407
+ | `'end'` | `{% endswitch %}` | End of switch |
408
+
409
+ `MLNode.conditionalChildNodes()` returns an array of `NodeListOf` arrays — one per conditional branch — so rules can analyze each branch independently. Note that `'each'` blocks do not start a new conditional mode (`'if'` or `'switch'`); they are flattened into `childNodes` and their content is treated as always-present rather than as an alternative branch. Only `'if'`/`'if:elseif'` and `'switch:case'` start new modes that generate null sentinels for the "empty branch" case.
353
410
 
354
411
  ## Plugin System
355
412
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,60 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-alpha.1](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.0...v5.0.0-alpha.1) (2026-02-22)
7
+
8
+ **Note:** Version bump only for package @markuplint/ml-core
9
+
10
+ # [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **ml-core:** improve detection of namespace ([5b507ad](https://github.com/markuplint/markuplint/commit/5b507ad7c19c5015b8ce587845d901e31dfa6518))
15
+ - treat orphaned end tags as bogus instead of plain text ([#1575](https://github.com/markuplint/markuplint/issues/1575)) ([557199a](https://github.com/markuplint/markuplint/commit/557199a6960ab35573a544f9a33c00e98eb9967e))
16
+ - use explicit `export type` for type-only re-exports ([7c77c05](https://github.com/markuplint/markuplint/commit/7c77c05619518c8d18a183132040f5b2cd0ab6ec))
17
+
18
+ - feat(ml-core)!: adapt DOM layer to simplified AST types ([5d92f2b](https://github.com/markuplint/markuplint/commit/5d92f2be75ce0d45823fb26f72588aecee278ba3))
19
+
20
+ ### Features
21
+
22
+ - delete htmx-parser, simplify alpine-parser, add migration guide and tests ([f8dbb09](https://github.com/markuplint/markuplint/commit/f8dbb090707d8cfbf3d859a9b868b2087064f89b))
23
+ - **ml-core:** add directive and IDL resolution to MLAttr constructor ([ba0ad66](https://github.com/markuplint/markuplint/commit/ba0ad66585c022cdb34fda8a8191bcc9af078e07))
24
+ - **ml-core:** add expandNamedRules for named rule groups in rules section ([7eed355](https://github.com/markuplint/markuplint/commit/7eed355075cee90b17a79c0f8a5b18213d1ce54e))
25
+ - **ml-core:** implement VirtualRule system for named nodeRules ([864f51d](https://github.com/markuplint/markuplint/commit/864f51d54dba26c6af2bc45eea3566db5f7d8e26))
26
+ - **ml-core:** require defaultValue for non-boolean rule types in createRule ([6c99908](https://github.com/markuplint/markuplint/commit/6c999087feff4fb8906cf47d564ee08ca8e5f450)), closes [#808](https://github.com/markuplint/markuplint/issues/808)
27
+ - **ml-core:** the each block skips linting in childNodes ([d5ca83d](https://github.com/markuplint/markuplint/commit/d5ca83d5ec6dc9b2f40b5d6599b07cc4746f3dca))
28
+ - **ml-core:** wire ruleCommonSettings through MLCore to Document ([28bb176](https://github.com/markuplint/markuplint/commit/28bb17601b983b3789b2ae200bd77ad887905cda))
29
+ - **ml-spec:** add declarative directivePatterns for parser-less framework support ([ceb9aa6](https://github.com/markuplint/markuplint/commit/ceb9aa67048e3a058b40a9e4d91eb903c8ff1861))
30
+
31
+ ### Performance Improvements
32
+
33
+ - **ml-core:** add memoization cache to MLElement.getAccessibleName() ([cdbe289](https://github.com/markuplint/markuplint/commit/cdbe289755312ee30e3f02171f42bf2c00412eea)), closes [#2179](https://github.com/markuplint/markuplint/issues/2179)
34
+
35
+ ### BREAKING CHANGES
36
+
37
+ - Multiple breaking changes to DOM API:
38
+
39
+ MLToken:
40
+
41
+ - Compute end positions via getEndCol/getEndLine helpers
42
+ instead of storing them as private fields
43
+ - Use \_astToken.offset/line/col directly
44
+
45
+ MLElement:
46
+
47
+ - Remove selfClosingSolidus property
48
+ - Add blockBehavior: MLASTBlockBehavior | null
49
+
50
+ MLBlock:
51
+
52
+ - Replace conditionalType with blockBehavior property
53
+
54
+ Node traversal:
55
+
56
+ - Use blockBehavior?.type instead of conditionalType
57
+
58
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
59
+
6
60
  ## [4.13.3](https://github.com/markuplint/markuplint/compare/@markuplint/ml-core@4.13.2...@markuplint/ml-core@4.13.3) (2026-02-10)
7
61
 
8
62
  **Note:** Version bump only for package @markuplint/ml-core
@@ -32,17 +32,18 @@ constructor(params: MLCoreParams)
32
32
 
33
33
  `MLFabric` 型はリンティング設定全体を定義します:
34
34
 
35
- | フィールド | 型 | 説明 |
36
- | --------------- | --------------------------------- | ----------------------------------------------------- |
37
- | `parser` | `MLParser` | パーサーインスタンス(例:`@markuplint/html-parser`) |
38
- | `ruleset` | `Ruleset` | 解決済みルール設定 |
39
- | `rules` | `readonly MLRule[]` | ルールインスタンスの配列 |
40
- | `locale` | `LocaleSet` | 違反メッセージのロケール |
41
- | `schemas` | `MLSchema` | HTML/ARIA 仕様タプル |
42
- | `parserOptions` | `ParserOptions` | パーサー設定オプション |
43
- | `severity` | `{ parseError?: SeverityOption }` | 重大度オーバーライド |
44
- | `pretenders` | `readonly Pretender[]` | コンポーネントから HTML へのマッピング |
45
- | `configErrors` | `readonly ConfigError[]` | 報告する設定エラー |
35
+ | フィールド | 型 | 説明 |
36
+ | -------------------- | --------------------------------- | ----------------------------------------------------- |
37
+ | `parser` | `MLParser` | パーサーインスタンス(例:`@markuplint/html-parser`) |
38
+ | `ruleset` | `Ruleset` | 解決済みルール設定 |
39
+ | `rules` | `readonly MLRule[]` | ルールインスタンスの配列 |
40
+ | `locale` | `LocaleSet` | 違反メッセージのロケール |
41
+ | `schemas` | `MLSchema` | HTML/ARIA 仕様タプル |
42
+ | `parserOptions` | `ParserOptions` | パーサー設定オプション |
43
+ | `severity` | `{ parseError?: SeverityOption }` | 重大度オーバーライド |
44
+ | `pretenders` | `readonly Pretender[]` | コンポーネントから HTML へのマッピング |
45
+ | `ruleCommonSettings` | `RuleCommonSettings` | すべてのルールにグローバルに適用される共通設定 |
46
+ | `configErrors` | `readonly ConfigError[]` | 報告する設定エラー |
46
47
 
47
48
  ### プロパティ
48
49
 
@@ -52,8 +53,9 @@ constructor(params: MLCoreParams)
52
53
 
53
54
  ### 構築フロー
54
55
 
55
- 1. `_parse()` `parser.parse(sourceCode, parserOptions)` を呼び出して `MLASTDocument` を生成
56
- 2. `_createDocument()` — AST `MLDocument` でラップ(ruleset、schemas、オプション付き)
56
+ 1. **Named nodeRule の展開** — `expandNamedNodeRules()` `nodeRules` `childNodeRules` の名前付きエントリを仮想 `MLRule` インスタンスに変換。展開前のオリジナルは `#originalNodeRules` / `#originalChildNodeRules` にホットリロード用に保持。名前空間 disable プレフィックスはワイルドカードエントリ(例: `"a11y/*": false`)から事前計算される。
57
+ 2. `_parse()` — `parser.parse(sourceCode, parserOptions)` を呼び出して `MLASTDocument` を生成
58
+ 3. `_createDocument()` — AST を `MLDocument` でラップ(ruleset、schemas、オプション付き)
57
59
 
58
60
  パースが失敗した場合、`document` は `MLDocument` の代わりに `ParserError` を保持します。
59
61
 
@@ -76,7 +78,7 @@ constructor(params: MLCoreParams)
76
78
  パースされた AST から `MLDocument` を作成します:
77
79
 
78
80
  ```typescript
79
- new Document(ast, ruleset, schemas, {
81
+ new Document(ast, ruleset, schemas, ruleCommonSettings, {
80
82
  filename,
81
83
  endTag,
82
84
  booleanish,
@@ -124,6 +126,7 @@ flowchart TD
124
126
  2. **未定義ルールの検出**: `setRuleNames`(設定内のルール)と `definedRuleName`(実際にロードされたルール)を比較。未定義ルールに対して `config-error` 警告を報告
125
127
  3. **設定エラー**: `configErrors` 配列を違反に変換
126
128
  4. **ルールループ**: 各ルールに対して:
129
+ - **仮想ルールの無効化チェック** — `rule.baseRuleId` がある場合、3つの条件を確認: 完全一致(`rules["alias"]: false`)、グループ無効化(`rules["groupName"]: false`)、名前空間ワイルドカード(`rules["scope/*"]: false`)。いずれかに該当した場合はスキップ。
127
130
  - `rule.getRuleInfo(ruleset, rule.name)` で有効性をチェック
128
131
  - `disabled && nodeRules.length === 0 && childNodeRules.length === 0` → スキップ
129
132
  - `rule.verify(document, locale, fix)` — ルールを実行
@@ -154,6 +157,7 @@ flowchart TD
154
157
 
155
158
  リンティング設定の部分更新:
156
159
 
160
+ - Named nodeRules を仮想ルールに再展開。`ruleset` が提供されていない場合、`#originalNodeRules` / `#originalChildNodeRules`(展開前のコピー)をソースとして使用し、仮想ルール化で変換済みのエントリを失うことを防ぐ。
157
161
  - `parserOptions` が変更された場合 → 完全な再パース(`_parse()` + `_createDocument()`)
158
162
  - それ以外 → `_createDocument()` のみ(既存の AST を再利用)
159
163
 
@@ -32,17 +32,18 @@ Where `MLCoreParams` extends `MLFabric` with:
32
32
 
33
33
  The `MLFabric` type defines the full linting configuration:
34
34
 
35
- | Field | Type | Description |
36
- | --------------- | --------------------------------- | ------------------------------------------------- |
37
- | `parser` | `MLParser` | Parser instance (e.g., `@markuplint/html-parser`) |
38
- | `ruleset` | `Ruleset` | Resolved rule configuration |
39
- | `rules` | `readonly MLRule[]` | Array of rule instances |
40
- | `locale` | `LocaleSet` | Locale for violation messages |
41
- | `schemas` | `MLSchema` | HTML/ARIA specification tuple |
42
- | `parserOptions` | `ParserOptions` | Parser configuration options |
43
- | `severity` | `{ parseError?: SeverityOption }` | Severity overrides |
44
- | `pretenders` | `readonly Pretender[]` | Component-to-HTML mappings |
45
- | `configErrors` | `readonly ConfigError[]` | Configuration errors to report |
35
+ | Field | Type | Description |
36
+ | -------------------- | --------------------------------- | ------------------------------------------------- |
37
+ | `parser` | `MLParser` | Parser instance (e.g., `@markuplint/html-parser`) |
38
+ | `ruleset` | `Ruleset` | Resolved rule configuration |
39
+ | `rules` | `readonly MLRule[]` | Array of rule instances |
40
+ | `locale` | `LocaleSet` | Locale for violation messages |
41
+ | `schemas` | `MLSchema` | HTML/ARIA specification tuple |
42
+ | `parserOptions` | `ParserOptions` | Parser configuration options |
43
+ | `severity` | `{ parseError?: SeverityOption }` | Severity overrides |
44
+ | `pretenders` | `readonly Pretender[]` | Component-to-HTML mappings |
45
+ | `ruleCommonSettings` | `RuleCommonSettings` | Common settings applied globally to all rules |
46
+ | `configErrors` | `readonly ConfigError[]` | Configuration errors to report |
46
47
 
47
48
  ### Properties
48
49
 
@@ -52,8 +53,9 @@ The `MLFabric` type defines the full linting configuration:
52
53
 
53
54
  ### Construction Flow
54
55
 
55
- 1. `_parse()` Invokes `parser.parse(sourceCode, parserOptions)` to produce `MLASTDocument`
56
- 2. `_createDocument()` — Wraps AST in `MLDocument` with ruleset, schemas, and options
56
+ 1. **Named nodeRule expansion** — `expandNamedNodeRules()` converts named entries in `nodeRules` and `childNodeRules` into virtual `MLRule` instances. The originals are preserved in `#originalNodeRules` / `#originalChildNodeRules` for hot-reload. Namespace disable prefixes are pre-computed from wildcard entries (e.g., `"a11y/*": false`).
57
+ 2. `_parse()` — Invokes `parser.parse(sourceCode, parserOptions)` to produce `MLASTDocument`
58
+ 3. `_createDocument()` — Wraps AST in `MLDocument` with ruleset, schemas, and options
57
59
 
58
60
  If parsing fails, `document` holds a `ParserError` instead of `MLDocument`.
59
61
 
@@ -76,7 +78,7 @@ If the parser throws:
76
78
  Creates `MLDocument` from the parsed AST:
77
79
 
78
80
  ```typescript
79
- new Document(ast, ruleset, schemas, {
81
+ new Document(ast, ruleset, schemas, ruleCommonSettings, {
80
82
  filename,
81
83
  endTag,
82
84
  booleanish,
@@ -124,6 +126,7 @@ flowchart TD
124
126
  2. **Undefined rule detection**: Compares `setRuleNames` (rules in config) vs `definedRuleName` (rules actually loaded). Reports `config-error` warnings for undefined rules
125
127
  3. **Config errors**: Converts `configErrors` array to violations
126
128
  4. **Rule loop**: For each rule:
129
+ - **Virtual rule disable check** — If `rule.baseRuleId` is set, checks three disable conditions: exact name match (`rules["alias"]: false`), group disable (`rules["groupName"]: false`), or namespace wildcard (`rules["scope/*"]: false`). Skips the rule if any condition is met.
127
130
  - `rule.getRuleInfo(ruleset, rule.name)` checks enablement
128
131
  - If `disabled && nodeRules.length === 0 && childNodeRules.length === 0` → skip
129
132
  - `rule.verify(document, locale, fix)` — executes the rule
@@ -154,6 +157,7 @@ Re-parses with new source code:
154
157
 
155
158
  Partially updates the linting configuration:
156
159
 
160
+ - Re-expands named nodeRules into virtual rules. When `ruleset` is not provided, uses `#originalNodeRules` / `#originalChildNodeRules` (pre-expansion copies) as the source to avoid losing named entries that were already transformed.
157
161
  - If `parserOptions` changed → full re-parse (`_parse()` + `_createDocument()`)
158
162
  - Otherwise → only `_createDocument()` (reuses existing AST)
159
163
 
@@ -206,5 +206,5 @@ pretender は `MLDocument` コンストラクタ(`src/ml-dom/node/document.ts`
206
206
  1. `document.debugMap()` の出力を期待される構造と比較
207
207
  2. パーサー出力を確認:同じソースをパーサーで直接パースして AST を検査
208
208
  3. ゴースト要素(暗黙の HTML/head/body)が正しく処理されているか確認
209
- 4. テンプレートエンジンの場合、`conditionalType` 付きの `MLBlock` ノードが期待される子をラップしているか確認
209
+ 4. テンプレートエンジンの場合、`blockBehavior` 付きの `MLBlock` ノードが期待される子をラップしているか確認
210
210
  5. `document.nodeList` でフラットノードリストを検査し、親子関係を確認
@@ -206,5 +206,5 @@ Changes to `@markuplint/ml-core` can affect:
206
206
  1. Compare `document.debugMap()` output with expected structure
207
207
  2. Check the parser output: parse the same source with the parser directly and inspect the AST
208
208
  3. Verify ghost elements (implicit HTML/head/body) are handled correctly
209
- 4. For template engines, check that `MLBlock` nodes with `conditionalType` wrap the expected children
209
+ 4. For template engines, check that `MLBlock` nodes with `blockBehavior` wrap the expected children
210
210
  5. Use `document.nodeList` to inspect the flat node list and verify parent-child relationships
@@ -24,6 +24,14 @@
24
24
  - `astToken.potentialName` が存在する場合 → `name` として使用する。そうでなければ `nameNode.raw` を使用する
25
25
  - `astToken.potentialValue` が存在する場合 → `value` として使用する。そうでなければ `valueNode.raw` を使用する
26
26
 
27
+ ### ディレクティブパターン解決
28
+
29
+ パーサーが `astToken.potentialName` を設定していない場合、`MLAttr` コンストラクタは spec(例: `@markuplint/vue-spec`、`@markuplint/svelte-spec`)からの `directivePatterns` をチェックします。パターンが存在する場合、生の属性名とコンパイル済みパターンで `resolveDirective()` が呼び出されます。最初にマッチしたパターンが `potentialName`、`isDynamicValue`、`isDirective`、`isDuplicatable` を決定します。
30
+
31
+ ### IDL 属性名解決
32
+
33
+ ディレクティブパターン解決の後、spec が `acceptedAttrNames` を設定しており(例: `@markuplint/react-spec` では `'idl'`、`@markuplint/svelte-spec` では `'both'`)、属性がディレクティブでない場合、コンストラクタは `@markuplint/parser-utils` の `searchIDLAttribute()` を呼び出して IDL プロパティ名を HTML コンテンツ属性名にマッピングします(例: `className` → `class`、`htmlFor` → `for`)。`'idl'` モードでは、IDL プロパティ名が候補として設定されます(例: `tabindex` → 「`tabIndex` の間違いでは?」)。`'both'` モードでは、コンテンツ名と IDL 名の両方が候補提案なしで受け入れられます。これはパーサーレベルではなくコアレベルの関心事です。
34
+
27
35
  ## トークン分解
28
36
 
29
37
  各属性は個々の `MLToken` インスタンスに分解されます:
@@ -24,6 +24,14 @@ The parser may provide resolved "potential" names and values for attributes wher
24
24
  - If `astToken.potentialName` exists → use it as `name`; otherwise use `nameNode.raw`
25
25
  - If `astToken.potentialValue` exists → use it as `value`; otherwise use `valueNode.raw`
26
26
 
27
+ ### Directive Pattern Resolution
28
+
29
+ When `astToken.potentialName` is not set by the parser, the `MLAttr` constructor checks for `directivePatterns` from the spec (e.g., `@markuplint/vue-spec`, `@markuplint/svelte-spec`). If patterns exist, `resolveDirective()` is called with the raw attribute name and the compiled patterns. The first matching pattern determines `potentialName`, `isDynamicValue`, `isDirective`, and `isDuplicatable`.
30
+
31
+ ### IDL Attribute Name Resolution
32
+
33
+ After directive pattern resolution, if the spec sets `acceptedAttrNames` (e.g., `'idl'` in `@markuplint/react-spec`, `'both'` in `@markuplint/svelte-spec`) and the attribute is not a directive, the constructor calls `searchIDLAttribute()` from `@markuplint/parser-utils` to map IDL property names to HTML content attribute names (e.g., `className` -> `class`, `htmlFor` -> `for`). In `'idl'` mode, the IDL property name is set as a candidate for suggestion (e.g., `tabindex` -> "Did you mean `tabIndex`?"). In `'both'` mode, both content and IDL names are accepted without suggestions. This is a core-level concern, not a parser-level one.
34
+
27
35
  ## Token Decomposition
28
36
 
29
37
  Each attribute is decomposed into individual `MLToken` instances: