@markuplint/ml-core 4.13.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 (92) hide show
  1. package/ARCHITECTURE.ja.md +524 -0
  2. package/ARCHITECTURE.md +524 -0
  3. package/CHANGELOG.md +52 -2
  4. package/README.md +5 -0
  5. package/SKILL.md +61 -0
  6. package/docs/linting-pipeline.ja.md +307 -0
  7. package/docs/linting-pipeline.md +307 -0
  8. package/docs/maintenance.ja.md +210 -0
  9. package/docs/maintenance.md +210 -0
  10. package/docs/ml-dom/attr.ja.md +103 -0
  11. package/docs/ml-dom/attr.md +103 -0
  12. package/docs/ml-dom/block.ja.md +272 -0
  13. package/docs/ml-dom/block.md +272 -0
  14. package/docs/ml-dom/document.ja.md +141 -0
  15. package/docs/ml-dom/document.md +141 -0
  16. package/docs/ml-dom/element.ja.md +176 -0
  17. package/docs/ml-dom/element.md +176 -0
  18. package/docs/ml-dom/helpers.ja.md +203 -0
  19. package/docs/ml-dom/helpers.md +203 -0
  20. package/docs/ml-dom/node.ja.md +199 -0
  21. package/docs/ml-dom/node.md +199 -0
  22. package/docs/ml-dom/others.ja.md +120 -0
  23. package/docs/ml-dom/others.md +120 -0
  24. package/docs/ml-dom/overview.ja.md +102 -0
  25. package/docs/ml-dom/overview.md +102 -0
  26. package/docs/ml-dom/pretender.ja.md +269 -0
  27. package/docs/ml-dom/pretender.md +269 -0
  28. package/docs/ml-dom/rule-mapping.ja.md +371 -0
  29. package/docs/ml-dom/rule-mapping.md +371 -0
  30. package/docs/ml-dom.ja.md +18 -0
  31. package/docs/ml-dom.md +18 -0
  32. package/docs/rule-system.ja.md +287 -0
  33. package/docs/rule-system.md +287 -0
  34. package/lib/convert-ruleset.d.ts +7 -0
  35. package/lib/convert-ruleset.js +7 -0
  36. package/lib/debug.d.ts +4 -0
  37. package/lib/debug.js +4 -0
  38. package/lib/index.d.ts +4 -3
  39. package/lib/index.js +1 -1
  40. package/lib/ml-core.d.ts +37 -1
  41. package/lib/ml-core.js +171 -82
  42. package/lib/ml-dom/helper/accname.d.ts +8 -0
  43. package/lib/ml-dom/helper/accname.js +71 -55
  44. package/lib/ml-dom/helper/create-node.js +1 -0
  45. package/lib/ml-dom/helper/get-indent.d.ts +4 -1
  46. package/lib/ml-dom/helper/get-indent.js +21 -30
  47. package/lib/ml-dom/node/attr.d.ts +65 -4
  48. package/lib/ml-dom/node/attr.js +151 -53
  49. package/lib/ml-dom/node/block.d.ts +23 -2
  50. package/lib/ml-dom/node/block.js +24 -1
  51. package/lib/ml-dom/node/child-node.d.ts +9 -0
  52. package/lib/ml-dom/node/child-node.js +9 -0
  53. package/lib/ml-dom/node/comment.d.ts +7 -0
  54. package/lib/ml-dom/node/comment.js +7 -0
  55. package/lib/ml-dom/node/document-fragment.d.ts +8 -0
  56. package/lib/ml-dom/node/document-fragment.js +8 -0
  57. package/lib/ml-dom/node/document-type.d.ts +22 -0
  58. package/lib/ml-dom/node/document-type.js +25 -0
  59. package/lib/ml-dom/node/document.d.ts +88 -7
  60. package/lib/ml-dom/node/document.js +128 -32
  61. package/lib/ml-dom/node/dom-token-list.js +17 -30
  62. package/lib/ml-dom/node/element-close-tag.js +1 -0
  63. package/lib/ml-dom/node/element.d.ts +151 -5
  64. package/lib/ml-dom/node/element.js +242 -50
  65. package/lib/ml-dom/node/node-store.js +6 -15
  66. package/lib/ml-dom/node/node.d.ts +19 -1
  67. package/lib/ml-dom/node/node.js +175 -166
  68. package/lib/ml-dom/node/parent-node.js +14 -30
  69. package/lib/ml-dom/node/rule-mapper.js +7 -20
  70. package/lib/ml-dom/node/text.d.ts +19 -0
  71. package/lib/ml-dom/node/text.js +21 -0
  72. package/lib/ml-dom/node/types.d.ts +68 -0
  73. package/lib/ml-dom/token/token.d.ts +42 -0
  74. package/lib/ml-dom/token/token.js +59 -39
  75. package/lib/ml-rule/create-rule.d.ts +17 -1
  76. package/lib/ml-rule/ml-rule-context.js +7 -11
  77. package/lib/ml-rule/ml-rule.d.ts +66 -1
  78. package/lib/ml-rule/ml-rule.js +95 -25
  79. package/lib/ml-rule/types.d.ts +41 -0
  80. package/lib/plugin/plugin.d.ts +8 -0
  81. package/lib/plugin/plugin.js +8 -0
  82. package/lib/plugin/types.d.ts +21 -0
  83. package/lib/ruleset/index.d.ts +10 -0
  84. package/lib/ruleset/index.js +13 -0
  85. package/lib/test/index.d.ts +42 -1
  86. package/lib/test/index.js +39 -2
  87. package/lib/types.d.ts +10 -1
  88. package/lib/violation-collector.d.ts +33 -0
  89. package/lib/violation-collector.js +48 -28
  90. package/lib/virtual-rule.d.ts +72 -0
  91. package/lib/virtual-rule.js +233 -0
  92. package/package.json +16 -13
@@ -0,0 +1,269 @@
1
+ # MLDOM における Pretender システム
2
+
3
+ **主要ソース:** `src/ml-dom/node/element.ts`(`pretending()`、`matchMLSelector()`、プロパティゲッター)
4
+ **関連:** `src/ml-dom/node/document.ts`(`_pretending()`)、`src/ml-dom/helper/accname.ts`(`getAccnameFromPretender()`)
5
+
6
+ ## 概要
7
+
8
+ Pretender システムは、著者定義コンポーネント(例: `<MyButton>`、`<AppLink>`)をリント時に標準 HTML 要素(例: `<button>`、`<a>`)として扱えるようにします。これは HTML セマンティクスに依存するルール — `wai-aria`(アクセシブル名/ロールの検証)や `permitted-contents`(コンテンツモデルの検証)など — がフレームワークコンポーネントに対して正しく動作するために不可欠です。
9
+
10
+ このシステムは、標準 HTML 要素を表す**仮想 MLElement** を作成し、元のコンポーネント要素と仮想要素の間に**双方向リンク**を確立することで機能します。MLDOM のプロパティゲッターやメソッドはこのリンクを確認し、適切な場合に透過的に pretender のデータを返します。
11
+
12
+ Pretender の設定構文については、[markuplint 設定ドキュメント](https://markuplint.dev/docs/configuration/properties#pretenders)を参照してください。
13
+
14
+ ## アーキテクチャ
15
+
16
+ ### 初期化フロー
17
+
18
+ Pretender の初期化は `MLDocument` のコンストラクション中に、すべてのノードが作成された後、ルールマッピングの前に行われます:
19
+
20
+ ```
21
+ MLDocument コンストラクタ
22
+ ├── 1. AST をパース → MLDOM ノードを作成(nodeList)
23
+ ├── 2. _pretending(pretenders) ← Pretender の初期化
24
+ │ └── nodeList の各 ELEMENT_NODE に対して:
25
+ │ └── element.pretending(pretenders)
26
+ └── 3. _ruleMapping(ruleset) ← ルールの割り当て
27
+ ```
28
+
29
+ この順序は意図的です:pretender はルールマッピングの**前に**確立される必要があります。なぜなら、ルールセレクタ(例: `button` をターゲットにした `nodeRules`)は pretender のアイデンティティに対してマッチする必要があるためです。
30
+
31
+ ### `pretending()` メソッド
32
+
33
+ `MLElement` の `pretending()` メソッドがコアの初期化ロジックです。ドキュメントのコンストラクション中に、要素ごとに1回呼び出されます。
34
+
35
+ **ステップ 1: マッチする設定を検索**
36
+
37
+ ```typescript
38
+ const pretenderConfig = pretenders?.find(option => this.matches(option.selector));
39
+ ```
40
+
41
+ Pretender 設定を反復し、CSS セレクタがこの要素にマッチする最初の設定を見つけます。
42
+
43
+ **ステップ 2: `as` 属性によるフォールバック**
44
+
45
+ ```typescript
46
+ const asAttrValue = this.getAttribute('as');
47
+ const pretenderElement =
48
+ pretenderConfig?.as ??
49
+ (this.elementType === 'html' || !asAttrValue ? null : { element: asAttrValue, inheritAttrs: true });
50
+ ```
51
+
52
+ 明示的な設定がマッチしないが、要素が非 HTML 要素(つまり `elementType !== 'html'`)で `as` 属性を持つ場合、その属性値をフォールバックとして使用します。これにより、明示的な設定なしで `<MyButton as="button">` が動作します。
53
+
54
+ **ステップ 3: Pretender 定義の解決**
55
+
56
+ `as` フィールドは単純な文字列(タグ名)か、詳細オプションを持つ `OriginalNode` オブジェクトのいずれかです:
57
+
58
+ | フィールド | 型 | 説明 |
59
+ | ------------------------ | ---------------- | ------------------------------------------------- |
60
+ | `element` | `string` | ターゲットの HTML タグ名(例: `"button"`、`"a"`) |
61
+ | `namespace` | `string?` | SVG 要素の場合 `'svg'`。デフォルトは `'html'` |
62
+ | `inheritAttrs` | `boolean?` | 元の要素の属性を仮想要素にコピーする |
63
+ | `attrs` | `Array?` | 仮想要素に追加する属性 |
64
+ | `attrs[].value.fromAttr` | `string?` | 元の要素の指定された属性から値を継承する |
65
+ | `aria` | `PretenderARIA?` | アクセシブル名の設定 |
66
+
67
+ **ステップ 4: 仮想要素の作成**
68
+
69
+ 合成 AST ノードで新しい `MLElement` が構築されます:
70
+
71
+ ```typescript
72
+ const as = new MLElement<T, O>(
73
+ {
74
+ ...this._astToken,
75
+ uuid: this.uuid + '_pretender',
76
+ raw: `<${nodeName}>`,
77
+ nodeName,
78
+ namespace,
79
+ elementType: 'html',
80
+ attributes, // inheritAttrs + attrs からマージ
81
+ },
82
+ this.ownerMLDocument,
83
+ );
84
+ ```
85
+
86
+ 仮想要素は元の要素の AST トークンをベースとして再利用し(ソース位置を継承)、タグ名、名前空間、属性をオーバーライドします。
87
+
88
+ **ステップ 5: 双方向リンクの設定**
89
+
90
+ ```typescript
91
+ as.pretenderContext = { type: 'origin', origin: this };
92
+ this.pretenderContext = { type: 'pretender', as, aria };
93
+ ```
94
+
95
+ **ステップ 6: 子ノードの共有**
96
+
97
+ ```typescript
98
+ as.resetChildren(this.childNodes);
99
+ ```
100
+
101
+ 仮想要素は元の要素の子ノードを受け取り、コンテンツモデルの検証が同じ子に対して動作するようにします。
102
+
103
+ ### `pretenderContext` 型
104
+
105
+ ```typescript
106
+ // 元の要素上(例: <MyButton>)
107
+ { type: 'pretender', as: MLElement, aria?: PretenderARIA }
108
+
109
+ // 仮想要素上(例: 合成された <button>)
110
+ { type: 'origin', origin: MLElement }
111
+
112
+ // 非参加
113
+ null
114
+ ```
115
+
116
+ ## プロパティの委譲
117
+
118
+ `pretenderContext.type === 'pretender'` の場合、いくつかの `MLElement` プロパティゲッターが仮想要素に委譲します:
119
+
120
+ ### 名前プロパティ
121
+
122
+ ```
123
+ <MyButton> (pretenderContext.type === 'pretender', as → <button>)
124
+
125
+ localName → this.pretenderContext.as.localName → "button"
126
+ nodeName → this.pretenderContext.as.nodeName → "BUTTON"
127
+ tagName → this.pretenderContext.as.nodeName → "BUTTON"
128
+ rawName → this._astToken.nodeName → "MyButton"(影響なし)
129
+ ```
130
+
131
+ `rawName` はソースを常に反映する唯一の名前プロパティです。これにより、元のタグ名を必要とするルール(例: 命名規則ルール)が引き続きアクセスできます。
132
+
133
+ ### 属性アクセス
134
+
135
+ | メソッド/プロパティ | Pretender 時の動作 |
136
+ | ----------------------------- | ------------------------------------------------------------------------------- |
137
+ | `attributes` | **仮想要素の**属性を返す(重複排除済み) |
138
+ | `getAttributeTokens()` | **仮想要素の**属性トークンを返す |
139
+ | `getAttribute(name)` | **仮想要素の**属性を検索する |
140
+ | `hasAttribute(name)` | **仮想要素**の `getAttribute` に委譲する |
141
+ | `getAttributePretended(name)` | Pretender コンテキストを**無視**し、**元の**要素の `#attributes` を直接検索する |
142
+
143
+ `getAttributePretended()` はアクセシブル名の計算のために特別に設計されています。pretender の `aria.name` が `{ fromAttr: "label" }` を指定している場合、システムは仮想 `<button>` からではなく、元の `<MyButton label="Save">` の `label` 属性を読み取る必要があります。
144
+
145
+ ### ルール設定
146
+
147
+ `rule` ゲッターは**逆方向**の特殊な委譲を持ちます:
148
+
149
+ ```typescript
150
+ // 仮想要素上(type === 'origin'):
151
+ get rule() {
152
+ return this.pretenderContext.origin.rule; // 元の要素に委譲
153
+ }
154
+ ```
155
+
156
+ これは、ルールが仮想要素で `element.rule` にアクセスすると、元の要素のルール設定を取得することを意味します。ルールはセレクタマッチングによってノードにマッピングされ、ルールマッピングはドキュメントの `nodeList` に存在する元の要素をターゲットにし、仮想要素はターゲットにしないためです。
157
+
158
+ ## セレクタマッチング
159
+
160
+ ### 2段階マッチング
161
+
162
+ `matchMLSelector()` は pretender 要素に対して2段階戦略を実装します:
163
+
164
+ ```
165
+ フェーズ 1: Pretender のアイデンティティとしてマッチ
166
+ セレクタ "button" → <button>(仮想)に対してマッチ → ヒット
167
+
168
+ フェーズ 2: 元のアイデンティティとしてマッチ(フェーズ 1 がミスした場合のみ)
169
+ セレクタ "MyButton" → 一時的に pretenderContext を null にする
170
+ → <MyButton>(元)に対してマッチ → ヒット
171
+ → pretenderContext を復元する
172
+ ```
173
+
174
+ これにより、両方のターゲティング戦略が機能します:
175
+
176
+ - **`button`** がマッチ:セマンティック要素をターゲットにしたルール(例: `<button>` の ARIA ルール)
177
+ - **`MyButton`** がマッチ:コンポーネントをターゲットにしたルール(例: `MyButton` の `nodeRules`)
178
+
179
+ ### ルールマッピングへの影響
180
+
181
+ `matchMLSelector()` はルールマッピングフェーズで `RuleMapper` によって使用されるため、pretender 対応のマッチングは以下を意味します:
182
+
183
+ - `selector: "button"` の `nodeRules` エントリは、`<MyButton>` が `<button>` として振る舞う場合にマッチする
184
+ - `selector: "MyButton"` の `nodeRules` エントリもマッチする。フェーズ 2 が元のアイデンティティにフォールバックするため
185
+
186
+ ## アクセシブル名の計算
187
+
188
+ `getAccname()` ヘルパーは `getAccnameFromPretender()` を通じて pretender システムと統合されます:
189
+
190
+ ```
191
+ getAccname(element)
192
+ ├── 1. @markuplint/ml-spec の get() を試行(標準 ARIA 計算)
193
+ ├── 2. getAccnameFromPretender(element) を試行 ← Pretender 固有
194
+ │ └── pretenderContext.type === 'pretender'
195
+ │ かつ pretenderContext.aria?.name が存在する場合:
196
+ │ ├── aria.name === true → "some-name(Pretender Options)"
197
+ │ └── aria.name === { fromAttr: "label" }
198
+ │ → el.getAttributePretended("label")
199
+ │ → 元の要素の属性から読み取る
200
+ ├── 3. aria-hidden/hidden をチェック → ""
201
+ ├── 4. accessibleNameFromContent をチェック → 子テキストの再帰的結合
202
+ └── 5. デフォルト → ""
203
+ ```
204
+
205
+ 重要な設計上の選択として、`getAccnameFromPretender()` は `el.getAttribute(attrName)` **ではなく** `el.getAttributePretended(attrName)` を呼び出し、pretender コンテキストをバイパスして元の要素から属性を読み取ります。これは `label` 属性が仮想 `<button>` ではなく元の `<MyButton label="Save">` に存在するためです。
206
+
207
+ ## `toString(fixed)` への影響
208
+
209
+ `fixed=true` の場合、`MLElement.toString()` は pretender 要素に対して**生のソース**を返します:
210
+
211
+ ```typescript
212
+ if (this.pretenderContext?.type === 'pretender') {
213
+ return this.raw;
214
+ }
215
+ ```
216
+
217
+ pretender は仮想的な構成物であるため、これは正しい動作です — pretender のアイデンティティに対して「修正」すべきソースコードはありません。元のタグ `<MyButton>` は出力で変更されずに残るべきです。
218
+
219
+ ## `walkOn()` への影響
220
+
221
+ Pretender システムによって作成された仮想要素はドキュメントの `nodeList` に**含まれません**。元の要素(`<MyButton>`)のみが走査されます。`localName`、`nodeName`、その他のプロパティが pretender に委譲するため、ルールは走査された要素にアクセスする際に pretender のアイデンティティを確認できます。
222
+
223
+ ```
224
+ document.walkOn('Element', el => {
225
+ // el は <MyButton> だが:
226
+ // el.localName === "button" (pretender から)
227
+ // el.rawName === "MyButton" (元の要素から)
228
+ // el.attributes は継承 + 設定された属性を含む
229
+ });
230
+ ```
231
+
232
+ ## データフロー図
233
+
234
+ ```
235
+ 設定(pretenders 配列)
236
+
237
+
238
+ MLDocument._pretending()
239
+
240
+ ▼(nodeList の各要素に対して)
241
+ MLElement.pretending()
242
+
243
+ ├─ セレクタで一致する設定を検索
244
+ ├─ フォールバック: `as` 属性を持つ非 HTML 要素
245
+
246
+
247
+ 仮想 MLElement を作成
248
+
249
+ ├─ タグ名、名前空間は設定から
250
+ ├─ 属性: inheritAttrs + 明示的属性
251
+ └─ 子: 元の要素と共有
252
+
253
+
254
+ 双方向リンク
255
+
256
+ ├─ Original.pretenderContext = { type: 'pretender', as: virtual, aria }
257
+ └─ Virtual.pretenderContext = { type: 'origin', origin: original }
258
+
259
+
260
+ プロパティ委譲が有効化
261
+
262
+ ├─ localName, nodeName, tagName → 仮想要素
263
+ ├─ attributes, getAttributeTokens() → 仮想要素
264
+ ├─ rawName, fixedNodeName → 元の要素(影響なし)
265
+ ├─ matchMLSelector() → 2段階(仮想を先に、次に元)
266
+ ├─ rule(仮想要素上) → 元の要素に委譲
267
+ ├─ getAccname() → pretender ARIA 設定 → getAttributePretended()
268
+ └─ toString(fixed) → raw(修正は適用されない)
269
+ ```
@@ -0,0 +1,269 @@
1
+ # Pretender System in MLDOM
2
+
3
+ **Primary source:** `src/ml-dom/node/element.ts` (`pretending()`, `matchMLSelector()`, property getters)
4
+ **Related:** `src/ml-dom/node/document.ts` (`_pretending()`), `src/ml-dom/helper/accname.ts` (`getAccnameFromPretender()`)
5
+
6
+ ## Overview
7
+
8
+ The pretender system allows authored components (e.g., `<MyButton>`, `<AppLink>`) to be treated as standard HTML elements (e.g., `<button>`, `<a>`) during linting. This is critical for rules that depend on HTML semantics — such as `wai-aria` (accessible name/role validation) and `permitted-contents` (content model validation) — to work correctly with framework components.
9
+
10
+ The system works by creating a **virtual MLElement** that represents the standard HTML element, and establishing a **bidirectional link** between the original component element and the virtual element. MLDOM property getters and methods check this link to transparently return the pretender's data when appropriate.
11
+
12
+ For pretender configuration syntax, see the [markuplint configuration documentation](https://markuplint.dev/docs/configuration/properties#pretenders).
13
+
14
+ ## Architecture
15
+
16
+ ### Initialization Flow
17
+
18
+ Pretender initialization happens during `MLDocument` construction, after all nodes are created but before rule mapping:
19
+
20
+ ```
21
+ MLDocument constructor
22
+ ├── 1. Parse AST → create MLDOM nodes (nodeList)
23
+ ├── 2. _pretending(pretenders) ← Pretender initialization
24
+ │ └── For each ELEMENT_NODE in nodeList:
25
+ │ └── element.pretending(pretenders)
26
+ └── 3. _ruleMapping(ruleset) ← Rule assignment
27
+ ```
28
+
29
+ This ordering is intentional: pretenders must be established **before** rule mapping, because rule selectors (e.g., `nodeRules` targeting `button`) need to match against the pretender identity.
30
+
31
+ ### `pretending()` Method
32
+
33
+ The `pretending()` method on `MLElement` is the core initialization logic. It is called once per element during document construction.
34
+
35
+ **Step 1: Find matching config**
36
+
37
+ ```typescript
38
+ const pretenderConfig = pretenders?.find(option => this.matches(option.selector));
39
+ ```
40
+
41
+ Iterate through the pretender configurations and find the first one whose CSS selector matches this element.
42
+
43
+ **Step 2: Fallback via `as` attribute**
44
+
45
+ ```typescript
46
+ const asAttrValue = this.getAttribute('as');
47
+ const pretenderElement =
48
+ pretenderConfig?.as ??
49
+ (this.elementType === 'html' || !asAttrValue ? null : { element: asAttrValue, inheritAttrs: true });
50
+ ```
51
+
52
+ If no explicit config matches but the element is a non-HTML element (i.e., `elementType !== 'html'`) and has an `as` attribute, use that attribute value as a fallback. This allows `<MyButton as="button">` to work without explicit config.
53
+
54
+ **Step 3: Resolve the pretender definition**
55
+
56
+ The `as` field can be either a simple string (tag name) or an `OriginalNode` object with detailed options:
57
+
58
+ | Field | Type | Description |
59
+ | ------------------------ | ---------------- | ---------------------------------------------------------------------- |
60
+ | `element` | `string` | Target HTML tag name (e.g., `"button"`, `"a"`) |
61
+ | `namespace` | `string?` | `'svg'` for SVG elements; defaults to `'html'` |
62
+ | `inheritAttrs` | `boolean?` | Copy the original element's attributes to the virtual element |
63
+ | `attrs` | `Array?` | Additional attributes to add to the virtual element |
64
+ | `attrs[].value.fromAttr` | `string?` | Inherit the value from the specified attribute on the original element |
65
+ | `aria` | `PretenderARIA?` | Accessible name configuration |
66
+
67
+ **Step 4: Create the virtual element**
68
+
69
+ A new `MLElement` is constructed with a synthetic AST node:
70
+
71
+ ```typescript
72
+ const as = new MLElement<T, O>(
73
+ {
74
+ ...this._astToken,
75
+ uuid: this.uuid + '_pretender',
76
+ raw: `<${nodeName}>`,
77
+ nodeName,
78
+ namespace,
79
+ elementType: 'html',
80
+ attributes, // merged from inheritAttrs + attrs
81
+ },
82
+ this.ownerMLDocument,
83
+ );
84
+ ```
85
+
86
+ The virtual element reuses the original element's AST token as a base (inheriting source position), but overrides the tag name, namespace, and attributes.
87
+
88
+ **Step 5: Set up bidirectional links**
89
+
90
+ ```typescript
91
+ as.pretenderContext = { type: 'origin', origin: this };
92
+ this.pretenderContext = { type: 'pretender', as, aria };
93
+ ```
94
+
95
+ **Step 6: Share children**
96
+
97
+ ```typescript
98
+ as.resetChildren(this.childNodes);
99
+ ```
100
+
101
+ The virtual element receives the original element's child nodes, so that content model validation works against the same children.
102
+
103
+ ### `pretenderContext` Type
104
+
105
+ ```typescript
106
+ // On the original element (e.g., <MyButton>)
107
+ { type: 'pretender', as: MLElement, aria?: PretenderARIA }
108
+
109
+ // On the virtual element (e.g., the synthetic <button>)
110
+ { type: 'origin', origin: MLElement }
111
+
112
+ // Not participating
113
+ null
114
+ ```
115
+
116
+ ## Property Delegation
117
+
118
+ When `pretenderContext.type === 'pretender'`, several `MLElement` property getters delegate to the virtual element:
119
+
120
+ ### Name Properties
121
+
122
+ ```
123
+ <MyButton> (pretenderContext.type === 'pretender', as → <button>)
124
+
125
+ localName → this.pretenderContext.as.localName → "button"
126
+ nodeName → this.pretenderContext.as.nodeName → "BUTTON"
127
+ tagName → this.pretenderContext.as.nodeName → "BUTTON"
128
+ rawName → this._astToken.nodeName → "MyButton" (unaffected)
129
+ ```
130
+
131
+ `rawName` is the only name property that always reflects the source. This allows rules that care about the original tag name (e.g., naming convention rules) to still access it.
132
+
133
+ ### Attribute Access
134
+
135
+ | Method/Property | Pretender behavior |
136
+ | ----------------------------- | ----------------------------------------------------------------------------------------- |
137
+ | `attributes` | Returns the **virtual element's** attributes (deduplicated) |
138
+ | `getAttributeTokens()` | Returns the **virtual element's** attribute tokens |
139
+ | `getAttribute(name)` | Searches the **virtual element's** attributes |
140
+ | `hasAttribute(name)` | Delegates to `getAttribute` on the **virtual element** |
141
+ | `getAttributePretended(name)` | **Ignores** pretender context; searches the **original** element's `#attributes` directly |
142
+
143
+ `getAttributePretended()` is specifically designed for the accessible name computation: when a pretender's `aria.name` specifies `{ fromAttr: "label" }`, the system needs to read the `label` attribute from the original `<MyButton label="Save">`, not from the virtual `<button>`.
144
+
145
+ ### Rule Configuration
146
+
147
+ The `rule` getter has special delegation in the **opposite direction**:
148
+
149
+ ```typescript
150
+ // On the virtual element (type === 'origin'):
151
+ get rule() {
152
+ return this.pretenderContext.origin.rule; // Delegate to the original
153
+ }
154
+ ```
155
+
156
+ This means when a rule accesses `element.rule` on the virtual element, it gets the rule configuration from the original element. This is because rules are mapped to nodes by selector matching, and the rule mapping targets the original elements (which exist in the document's `nodeList`), not the virtual elements.
157
+
158
+ ## Selector Matching
159
+
160
+ ### Two-Phase Matching
161
+
162
+ `matchMLSelector()` implements a two-phase strategy for pretender elements:
163
+
164
+ ```
165
+ Phase 1: Match as the pretender identity
166
+ selector "button" → match against <button> (virtual) → HIT
167
+
168
+ Phase 2: Match as the original identity (only if Phase 1 missed)
169
+ selector "MyButton" → temporarily null out pretenderContext
170
+ → match against <MyButton> (original) → HIT
171
+ → restore pretenderContext
172
+ ```
173
+
174
+ This ensures both targeting strategies work:
175
+
176
+ - **`button`** matches: rules that target the semantic element (e.g., ARIA rules for `<button>`)
177
+ - **`MyButton`** matches: rules that target the component (e.g., `nodeRules` for `MyButton`)
178
+
179
+ ### Impact on Rule Mapping
180
+
181
+ Since `matchMLSelector()` is used by `RuleMapper` during the rule mapping phase, pretender-aware matching means:
182
+
183
+ - A `nodeRules` entry with `selector: "button"` will match `<MyButton>` if it pretends to be `<button>`
184
+ - A `nodeRules` entry with `selector: "MyButton"` will also match, because Phase 2 falls back to the original identity
185
+
186
+ ## Accessible Name Computation
187
+
188
+ The `getAccname()` helper integrates with the pretender system via `getAccnameFromPretender()`:
189
+
190
+ ```
191
+ getAccname(element)
192
+ ├── 1. Try @markuplint/ml-spec get() (standard ARIA computation)
193
+ ├── 2. Try getAccnameFromPretender(element) ← Pretender-specific
194
+ │ └── If pretenderContext.type === 'pretender'
195
+ │ and pretenderContext.aria?.name exists:
196
+ │ ├── aria.name === true → "some-name(Pretender Options)"
197
+ │ └── aria.name === { fromAttr: "label" }
198
+ │ → el.getAttributePretended("label")
199
+ │ → reads from the ORIGINAL element's attributes
200
+ ├── 3. Check aria-hidden/hidden → ""
201
+ ├── 4. Check accessibleNameFromContent → recursive child text
202
+ └── 5. Default → ""
203
+ ```
204
+
205
+ The key design choice is that `getAccnameFromPretender()` calls `el.getAttributePretended(attrName)` — **not** `el.getAttribute(attrName)` — to read the attribute from the original element, bypassing the pretender context. This is because the `label` attribute exists on `<MyButton label="Save">`, not on the virtual `<button>`.
206
+
207
+ ## Effect on `toString(fixed)`
208
+
209
+ When `fixed=true`, `MLElement.toString()` returns the **raw source** for pretender elements:
210
+
211
+ ```typescript
212
+ if (this.pretenderContext?.type === 'pretender') {
213
+ return this.raw;
214
+ }
215
+ ```
216
+
217
+ This is correct because the pretender is a virtual construct — there is no source code to "fix" for the pretender identity. The original tag `<MyButton>` should remain unchanged in the output.
218
+
219
+ ## Effect on `walkOn()`
220
+
221
+ Virtual elements created by the pretender system are **not** in the document's `nodeList`. Only the original elements (`<MyButton>`) are walked. Since `localName`, `nodeName`, and other properties delegate to the pretender, rules see the pretender identity when accessing the walked element.
222
+
223
+ ```
224
+ document.walkOn('Element', el => {
225
+ // el is <MyButton>, but:
226
+ // el.localName === "button" (from pretender)
227
+ // el.rawName === "MyButton" (from original)
228
+ // el.attributes includes inherited + configured attrs
229
+ });
230
+ ```
231
+
232
+ ## Data Flow Diagram
233
+
234
+ ```
235
+ Configuration (pretenders array)
236
+
237
+
238
+ MLDocument._pretending()
239
+
240
+ ▼ (for each element in nodeList)
241
+ MLElement.pretending()
242
+
243
+ ├─ Find matching config by selector
244
+ ├─ Fallback: non-HTML element with `as` attribute
245
+
246
+
247
+ Create virtual MLElement
248
+
249
+ ├─ Tag name, namespace from config
250
+ ├─ Attributes: inheritAttrs + explicit attrs
251
+ └─ Children: shared with original
252
+
253
+
254
+ Bidirectional link
255
+
256
+ ├─ Original.pretenderContext = { type: 'pretender', as: virtual, aria }
257
+ └─ Virtual.pretenderContext = { type: 'origin', origin: original }
258
+
259
+
260
+ Property delegation active
261
+
262
+ ├─ localName, nodeName, tagName → virtual
263
+ ├─ attributes, getAttributeTokens() → virtual
264
+ ├─ rawName, fixedNodeName → original (unaffected)
265
+ ├─ matchMLSelector() → two-phase (virtual first, then original)
266
+ ├─ rule (on virtual) → delegates to original
267
+ ├─ getAccname() → pretender ARIA config → getAttributePretended()
268
+ └─ toString(fixed) → raw (no fix applied)
269
+ ```