@markuplint/ml-config 4.8.14 → 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.
@@ -0,0 +1,314 @@
1
+ # @markuplint/ml-config
2
+
3
+ ## 概要
4
+
5
+ `@markuplint/ml-config` は markuplint の設定システムの中核パッケージです。`Config` 型階層、複数の設定レイヤー(ベース、extends、overrides)を一つの最適化された設定にマージするアルゴリズム、キャプチャ変数をルール設定に注入する Mustache テンプレートレンダリングシステムを提供します。`@markuplint/file-resolver`(設定ファイルの読み込みと解決)と `@markuplint/ml-core`(マージ済み設定を使用してルールを適用)の間に位置します。
6
+
7
+ ## ディレクトリ構成
8
+
9
+ ```
10
+ src/
11
+ ├── index.ts — すべての公開 API を再エクスポート
12
+ ├── types.ts — 全型定義(Config, Rule, Pretender, Violation 等)
13
+ ├── merge-config.ts — マージアルゴリズム(mergeConfig, mergeRule, ヘルパー関数)
14
+ ├── merge-config.spec.ts — マージアルゴリズムのテスト
15
+ ├── utils.ts — テンプレートレンダリング、ルール正規化、型ガード
16
+ └── utils.spec.ts — ユーティリティのテスト
17
+ ```
18
+
19
+ ## 型システム
20
+
21
+ ### Config 型階層
22
+
23
+ ```mermaid
24
+ classDiagram
25
+ class Config {
26
+ +$schema?: string
27
+ +extends?: string | string[]
28
+ +plugins?: (PluginConfig | string)[]
29
+ +parser?: ParserConfig
30
+ +parserOptions?: ParserOptions
31
+ +specs?: SpecConfig
32
+ +excludeFiles?: string[]
33
+ +severity?: SeverityOptions
34
+ +pretenders?: Pretender[] | PretenderDetails
35
+ +rules?: Rules
36
+ +nodeRules?: NodeRule[]
37
+ +childNodeRules?: ChildNodeRule[]
38
+ +overrideMode?: "merge" | "reset"
39
+ +overrides?: Record~string, OverrideConfig~
40
+ }
41
+
42
+ class OverrideConfig {
43
+ <<Omit Config NoInherit>>
44
+ }
45
+
46
+ class OptimizedConfig {
47
+ +plugins?: PluginConfig[]
48
+ +pretenders?: PretenderDetails
49
+ +overrides?: Record~string, OptimizedOverrideConfig~
50
+ }
51
+
52
+ Config --> OverrideConfig : "Omit $schema, extends,\noverrideMode, overrides"
53
+ Config --> OptimizedConfig : "mergeConfig()"
54
+ OverrideConfig --> OptimizedOverrideConfig : "mergeConfig()"
55
+ ```
56
+
57
+ ### Config から OptimizedConfig への変換
58
+
59
+ | フィールド | Config | OptimizedConfig | 変換 |
60
+ | ------------ | --------------------------------- | ----------------------------------------- | -------------------------------------- |
61
+ | `plugins` | `(PluginConfig \| string)[]` | `PluginConfig[]` | 文字列を `{name}` オブジェクトに正規化 |
62
+ | `pretenders` | `Pretender[] \| PretenderDetails` | `PretenderDetails` | 配列を `{data: [...]}` に変換 |
63
+ | `extends` | `string \| string[]` | 削除 | マージ後は不要 |
64
+ | `$schema` | `string` | 削除 | メタデータのみ |
65
+ | `overrides` | `Record<string, OverrideConfig>` | `Record<string, OptimizedOverrideConfig>` | 各値を再帰的にマージ |
66
+
67
+ ### ルール型の3形式
68
+
69
+ ルールは3つの形式で設定できます:
70
+
71
+ | 形式 | 型 | 例 | 意味 |
72
+ | ------- | ----------------- | ------------------------------------ | --------------------------- |
73
+ | Boolean | `boolean` | `true` / `false` | デフォルトで有効化 / 無効化 |
74
+ | Value | `RuleConfigValue` | `"always"`, `["a","b"]`, `null` | ショートハンド値 |
75
+ | Object | `RuleConfig<T,O>` | `{severity, value, options, reason}` | フル設定 |
76
+
77
+ ```ts
78
+ type Rule<T, O> = RuleConfig<T, O> | Readonly<T> | boolean;
79
+
80
+ type RuleConfig<T, O> = {
81
+ severity?: Severity; // 'error' | 'warning' | 'info'
82
+ value?: Readonly<T>;
83
+ options?: Readonly<O>;
84
+ reason?: string;
85
+ };
86
+ ```
87
+
88
+ ### NodeRule / ChildNodeRule
89
+
90
+ - `NodeRule` -- CSS セレクタ、正規表現セレクタ、ARIA ロール、カテゴリ、obsolete フラグで対象ノードを限定し、ルール設定をオーバーライド。オプションの `name`(`/` を含む必要あり)で named nodeRule として仮想ルールに展開可能。オプションの `specConformance`(`'normative'` または `'non-normative'`)を下流ツールやレポート向けのメタデータとして設定
91
+ - `ChildNodeRule` -- `NodeRule` と類似だが子ノードを対象とする。`inheritance` フラグで子孫への継承を制御。`name` と `specConformance`(メタデータ)による named nodeRule 展開もサポート
92
+
93
+ ### Pretender 型
94
+
95
+ - `Pretender` -- CSS セレクタを使用してカスタム要素を標準要素に見せかける。`as` フィールドで要素名または詳細な `OriginalNode` を指定
96
+ - `OriginalNode` -- 要素名、スロット、名前空間、属性、継承属性、ARIA プロパティを定義
97
+ - `PretenderDetails` -- マージ後に使用される正規化形式 `{data?, files?, imports?}`
98
+
99
+ ## マージアルゴリズム
100
+
101
+ このパッケージの中核です。`mergeConfig()` 関数はプロパティごとの戦略で2つの設定を統合します。
102
+
103
+ ### mergeConfig() の全体フロー
104
+
105
+ ```ts
106
+ mergeConfig(a: Config, b?: Config): OptimizedConfig
107
+ ```
108
+
109
+ ```mermaid
110
+ flowchart TD
111
+ A["入力: ベース (a) + オーバーライド (b)"] --> B{"b が指定されている?"}
112
+ B -->|Yes| C["deleteExtendsProp = true"]
113
+ B -->|No| C2["b = {}, deleteExtendsProp = false"]
114
+ C --> D["スプレッド: {...a, ...b}\n(プリミティブフィールドは右辺優先)"]
115
+ C2 --> D
116
+ D --> E["プロパティ別マージ戦略を適用\n(下記テーブル参照)"]
117
+ E --> F{"deleteExtendsProp?"}
118
+ F -->|Yes| G["結果から extends を削除"]
119
+ F -->|No| H["extends を保持"]
120
+ G --> I["deleteUndefProp()\nundefined プロパティを除去"]
121
+ H --> I
122
+ I --> J["OptimizedConfig を返却"]
123
+ ```
124
+
125
+ ### プロパティ別マージ戦略テーブル
126
+
127
+ | プロパティ | 戦略 | ヘルパー関数 | 詳細 |
128
+ | ---------------- | -------------------------- | ---------------------------------------------------- | --------------------------------------------- |
129
+ | `plugins` | 結合+重複排除+正規化 | `concatArray(uniquely=true, comparePropName='name')` | 同名プラグインは settings を shallow merge |
130
+ | `parser` | オブジェクト shallow merge | `mergeObject()` | `{...a, ...b}` で右辺優先 |
131
+ | `parserOptions` | オブジェクト shallow merge | `mergeObject()` | 同上 |
132
+ | `specs` | オブジェクト shallow merge | `mergeObject()` | 同上 |
133
+ | `excludeFiles` | 結合+重複排除 | `concatArray(uniquely=true)` | 単純な値の重複排除 |
134
+ | `severity` | オブジェクト shallow merge | `mergeObject()` | parser と同様 |
135
+ | `pretenders` | セマンティックマージ | `mergePretenders()` | files/imports: 上書き、data: 追加 |
136
+ | `rules` | ルール別マージ | `mergeRules()` → `mergeRule()` | **最も複雑 -- 次節で詳述** |
137
+ | `nodeRules` | 結合+名前で重複排除 | `concatArray(uniquely=true, comparePropName='name')` | 名前付きエントリは重複排除、無名は追加 |
138
+ | `childNodeRules` | 結合+名前で重複排除 | `concatArray(uniquely=true, comparePropName='name')` | nodeRules と同様 |
139
+ | `overrideMode` | 右辺優先 | `b.overrideMode ?? a.overrideMode` | 単純な優先順位 |
140
+ | `overrides` | キー別再帰マージ | `mergeOverrides()` | 各キーに対して `mergeConfig()` を再帰呼び出し |
141
+ | `extends` | 結合→削除 | `concatArray()` | マージ後に結果から削除 |
142
+
143
+ ### mergeRule() -- ルールマージの詳細
144
+
145
+ ```ts
146
+ mergeRule(a: Nullable<AnyRule>, b: AnyRule): AnyRule
147
+ ```
148
+
149
+ 最も複雑なマージロジックを処理する関数です。両方の入力はまず `optimizeRule()` で正規化されます。
150
+
151
+ ```mermaid
152
+ flowchart TD
153
+ Start["mergeRule(a, b)"] --> OptAB["optimizeRule() で両方を正規化"]
154
+ OptAB --> ChkFalse{"b === false OR\nb.value === false?"}
155
+ ChkFalse -->|Yes| RetFalse["return false\n(絶対無効化)"]
156
+ ChkFalse -->|No| ChkAUndef{"a === undefined?"}
157
+ ChkAUndef -->|Yes| RetB["return b"]
158
+ ChkAUndef -->|No| ChkBUndef{"b === undefined?"}
159
+ ChkBUndef -->|Yes| RetA["return a"]
160
+ ChkBUndef -->|No| ChkBVal{"b は Value?\n(primitive/null/array)"}
161
+ ChkBVal -->|Yes| ChkAVal{"a は Value?"}
162
+ ChkAVal -->|Yes| RetBVal["return b\n(右辺優先、\n配列も上書き)"]
163
+ ChkAVal -->|No| MergeValObj["a の severity/reason を保持\nvalue を b で上書き"]
164
+ ChkBVal -->|No| MergeObj["severity: b ?? a\nvalue: b ?? a\noptions: mergeObject(a, b)\nreason: b ?? a"]
165
+ ```
166
+
167
+ **重要な設計判断:**
168
+
169
+ 1. **`false` は絶対無効化** -- override が `false`(または `{value: false}`)なら、base が何であっても結果は常に `false`
170
+ 2. **配列値は上書き** -- `["a","b"]` + `["c","d"]` は `["c","d"]` になる(右辺優先)。ESLint や Biome と一貫した動作
171
+ 3. **options は shallow merge** -- severity、value、reason は右辺優先だが、options のみ `mergeObject()`(`{...a, ...b}` による shallow merge)を使用
172
+
173
+ ### ヘルパー関数
174
+
175
+ #### concatArray(a, b, uniquely?, comparePropName?)
176
+
177
+ オプションの重複排除付きで2つの配列を連結:
178
+
179
+ - `uniquely=false` -- 単純な連結、重複排除なし
180
+ - `uniquely=true`、`comparePropName` なし -- 完全一致で重複排除
181
+ - `uniquely=true`、`comparePropName` あり -- 指定プロパティ名で重複排除。同名オブジェクトはオブジェクトスプレッドで shallow merge(例: プラグインの settings)
182
+ - 空の結果には `undefined` を返す
183
+
184
+ #### mergeObject(a, b)
185
+
186
+ `{...a, ...b}` による shallow merge。トップレベルで右辺の値が優先。結果から undefined プロパティを除去。
187
+
188
+ #### mergeOverrides(a, b)
189
+
190
+ 両方の override レコードから全キーの集合を取得。各キーに対して `mergeConfig(a[key], b[key])` を再帰呼び出し。各結果から `$schema`、`extends`、`overrides` を削除(これらはトップレベル専用プロパティ)。
191
+
192
+ #### mergePretenders(a, b)
193
+
194
+ 配列形式の pretenders を正規化形式 `PretenderDetails`(`{data: [...]}`)に変換してからセマンティックマージ: `files`/`imports` は上書き(右辺優先)、`data` は追加(連結)。
195
+
196
+ ## テンプレートレンダリングシステム
197
+
198
+ ### provideValue(template, data)
199
+
200
+ Mustache テンプレート文字列を提供されたデータでレンダリング:
201
+
202
+ - テンプレートに変数がない -- テンプレートをそのまま返す
203
+ - 変数があるが data にマッチするキーがない -- `undefined` を返す
204
+ - 変数があり data にマッチするキーがある -- レンダリング結果を返す
205
+
206
+ ### exchangeValueOnRule(rule, data)
207
+
208
+ ルール設定内のすべての文字列値に Mustache テンプレートレンダリングを適用:
209
+
210
+ - **value** -- 文字列値はレンダリングされる。配列の各要素も個別にレンダリング
211
+ - **options** -- options オブジェクト内のすべての文字列値を再帰的にレンダリング
212
+ - **reason** -- 文字列としてレンダリング
213
+
214
+ この関数は `nodeRules` / `childNodeRules` の `regexSelector` で使用され、キャプチャグループ(`$0`、`$1`、`dataName` 等の名前付きキャプチャ)がテンプレート変数としてルール設定に注入されます。
215
+
216
+ ## ユーティリティ関数
217
+
218
+ | 関数 | 目的 |
219
+ | --------------------- | ------------------------------------------------------------------------------------------------- |
220
+ | `cleanOptions()` | 標準フィールド(`severity`、`value`、`options`、`reason`)を抽出して undefined プロパティを除去 |
221
+ | `isRuleConfigValue()` | 型ガード: プリミティブ、`null`、配列(= `RuleConfig` オブジェクトではない)に対して `true` を返す |
222
+ | `deleteUndefProp()` | プレーンオブジェクトから `undefined` 値のプロパティをすべて in-place で削除 |
223
+
224
+ ## 主要ソースファイル
225
+
226
+ | ファイル | 目的 |
227
+ | --------------------- | ------------------------------------------------------------------------------------------------------- |
228
+ | `src/types.ts` | 全型定義(Config, Rule, Pretender, Violation 等) |
229
+ | `src/merge-config.ts` | `mergeConfig()`、`mergeRule()`、全ヘルパー関数 |
230
+ | `src/utils.ts` | `provideValue()`、`exchangeValueOnRule()`、`cleanOptions()`、`isRuleConfigValue()`、`deleteUndefProp()` |
231
+ | `src/index.ts` | 全公開 API の再エクスポート |
232
+
233
+ ## 外部依存関係
234
+
235
+ | 依存パッケージ | 用途 |
236
+ | ---------------------- | --------------------------------------------------- |
237
+ | `@markuplint/ml-ast` | `ParserOptions` 型(型のみ) |
238
+ | `@markuplint/selector` | `RegexSelector` 型(再エクスポート) |
239
+ | `@markuplint/shared` | `Nullable` ユーティリティ型 |
240
+ | `is-plain-object` | `deleteUndefProp()` でのプレーンオブジェクト判定 |
241
+ | `mustache` | `provideValue()` のテンプレートレンダリングエンジン |
242
+ | `type-fest` | `Writable` ユーティリティ型 |
243
+
244
+ ## 統合ポイント
245
+
246
+ ```mermaid
247
+ flowchart LR
248
+ subgraph upstream ["上流"]
249
+ fileResolver["@markuplint/file-resolver\n(設定ファイル読み込み,\nextends チェーン解決)"]
250
+ end
251
+
252
+ subgraph pkg ["@markuplint/ml-config"]
253
+ mergeConfig["mergeConfig()\n(マージアルゴリズム)"]
254
+ types["Config 型定義"]
255
+ templates["テンプレートレンダリング"]
256
+ end
257
+
258
+ subgraph downstream ["下流"]
259
+ mlCore["@markuplint/ml-core\n(OptimizedConfig を使用して\nルールを適用)"]
260
+ rules["@markuplint/rules\n(Rule<T,O>,\nRuleConfig<T,O> 型を使用)"]
261
+ end
262
+
263
+ fileResolver -->|"各 extends レイヤーで\nmergeConfig() を呼び出し"| mergeConfig
264
+ mergeConfig -->|"OptimizedConfig を生成"| mlCore
265
+ types -->|"Rule, RuleConfig 型"| rules
266
+ ```
267
+
268
+ ### 上流
269
+
270
+ - **`@markuplint/file-resolver`** -- 設定ファイルを読み込み、extends チェーンを解決し、`mergeConfig()` を呼び出してレイヤーを統合
271
+
272
+ ### 下流
273
+
274
+ - **`@markuplint/ml-core`** -- マージ済みの `OptimizedConfig` を受け取り、パース済みドキュメントにルールを適用
275
+ - **`@markuplint/rules`** -- `Rule<T,O>` と `RuleConfig<T,O>` 型を使用してルール実装を定義
276
+
277
+ ## 設計判断
278
+
279
+ ### Flat Config を採用しない理由
280
+
281
+ ESLint の Flat Config アプローチを評価した結果、markuplint には不適と判断しました:
282
+
283
+ - **ESLint 自体が 2025年3月に Flat Config へ `extends` を再追加** -- 純粋な Flat アプローチは JavaScript ツールでも不十分だった
284
+ - **markuplint は JSON ベース** -- Flat Config は JavaScript を前提とする。HTML/マークアップ開発者(主要ユーザー層)にとって、JSON のスキーマ検証と言語非依存性は大きなメリット
285
+ - **markuplint の `nodeRules`/`childNodeRules` は CSS セレクタベース** -- Flat Config のファイルパターンモデルに対応する仕組みがない
286
+ - **ESLint v9 への移行はコミュニティに大きな痛みを与えた** -- 段階的な移行には自動化ツールと数年のエコシステム適応が必要だった
287
+
288
+ **結論:** JSON ベースの `extends` マージ戦略の改善が markuplint にとって最適なアプローチ。
289
+
290
+ ### マージ戦略の原則
291
+
292
+ 配列の扱いには論理的な区別があります:
293
+
294
+ | 配列の種類 | 例 | マージ動作 | 根拠 |
295
+ | ------------------------------ | -------------------------------------- | ---------- | ---------------------- |
296
+ | **トップレベルのコレクション** | `plugins`, `excludeFiles`, `nodeRules` | 累積 | 独立したアイテムの集合 |
297
+ | **ルール値** | `["allowed-tag-1", "allowed-tag-2"]` | 上書き | 1つのルールの設定値 |
298
+
299
+ これは ESLint と Biome の動作と一貫しており、ルール値はより具体的な設定で常に上書きされます。
300
+
301
+ ### Deep Merge ではなく Shallow Merge
302
+
303
+ | ツール | ルール options のマージ | 根拠 |
304
+ | ---------- | ----------------------------- | -------------------------------------------------------------------------- |
305
+ | ESLint | 完全リプレース | 最もシンプルだが驚きがある |
306
+ | Biome | Deep merge(Merge trait経由) | 完全な柔軟性、高い複雑性 |
307
+ | markuplint | **Shallow merge** | 中間地点: トップレベルのキーはマージ、ネストされたオブジェクトはリプレース |
308
+
309
+ `deepmerge` ライブラリを削除し、シンプルなオブジェクトスプレッド(`{...a, ...b}`)に置き換えました。markuplint の設定でマージされるすべてのオブジェクト(parser、specs、parserOptions、severity、プラグイン設定、ルールオプション)はフラットなキーバリューマップであるため、これで十分です。
310
+
311
+ ## ドキュメントマップ
312
+
313
+ - [マイグレーションガイド](../../docs/migration/v4-v5/config.ja.md) -- メジャーバージョン間の破壊的変更
314
+ - [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング
@@ -0,0 +1,314 @@
1
+ # @markuplint/ml-config
2
+
3
+ ## Overview
4
+
5
+ `@markuplint/ml-config` is the configuration system core for markuplint. It provides the `Config` type hierarchy, the merge algorithm that combines multiple configuration layers (base, extends, overrides) into a single optimized config, and a Mustache template rendering system for injecting captured variables into rule settings. The package sits between `@markuplint/file-resolver` (which reads and resolves config files) and `@markuplint/ml-core` (which applies rules using the merged config).
6
+
7
+ ## Directory Structure
8
+
9
+ ```
10
+ src/
11
+ ├── index.ts — Re-exports all public APIs
12
+ ├── types.ts — All type definitions (Config, Rule, Pretender, Violation, etc.)
13
+ ├── merge-config.ts — Merge algorithm (mergeConfig, mergeRule, helpers)
14
+ ├── merge-config.spec.ts — Merge algorithm tests
15
+ ├── utils.ts — Template rendering, rule normalization, type guards
16
+ └── utils.spec.ts — Utils tests
17
+ ```
18
+
19
+ ## Type System
20
+
21
+ ### Config Type Hierarchy
22
+
23
+ ```mermaid
24
+ classDiagram
25
+ class Config {
26
+ +$schema?: string
27
+ +extends?: string | string[]
28
+ +plugins?: (PluginConfig | string)[]
29
+ +parser?: ParserConfig
30
+ +parserOptions?: ParserOptions
31
+ +specs?: SpecConfig
32
+ +excludeFiles?: string[]
33
+ +severity?: SeverityOptions
34
+ +pretenders?: Pretender[] | PretenderDetails
35
+ +rules?: Rules
36
+ +nodeRules?: NodeRule[]
37
+ +childNodeRules?: ChildNodeRule[]
38
+ +overrideMode?: "merge" | "reset"
39
+ +overrides?: Record~string, OverrideConfig~
40
+ }
41
+
42
+ class OverrideConfig {
43
+ <<Omit Config NoInherit>>
44
+ }
45
+
46
+ class OptimizedConfig {
47
+ +plugins?: PluginConfig[]
48
+ +pretenders?: PretenderDetails
49
+ +overrides?: Record~string, OptimizedOverrideConfig~
50
+ }
51
+
52
+ Config --> OverrideConfig : "Omit $schema, extends,\noverrideMode, overrides"
53
+ Config --> OptimizedConfig : "mergeConfig()"
54
+ OverrideConfig --> OptimizedOverrideConfig : "mergeConfig()"
55
+ ```
56
+
57
+ ### Config to OptimizedConfig Conversion
58
+
59
+ | Field | Config | OptimizedConfig | Conversion |
60
+ | ------------ | --------------------------------- | ----------------------------------------- | -------------------------------------- |
61
+ | `plugins` | `(PluginConfig \| string)[]` | `PluginConfig[]` | Strings normalized to `{name}` objects |
62
+ | `pretenders` | `Pretender[] \| PretenderDetails` | `PretenderDetails` | Arrays converted to `{data: [...]}` |
63
+ | `extends` | `string \| string[]` | Removed | No longer needed after merging |
64
+ | `$schema` | `string` | Removed | Metadata only |
65
+ | `overrides` | `Record<string, OverrideConfig>` | `Record<string, OptimizedOverrideConfig>` | Each value recursively merged |
66
+
67
+ ### Rule Type Forms
68
+
69
+ A rule can be configured in three forms:
70
+
71
+ | Form | Type | Example | Meaning |
72
+ | ------- | ----------------- | ------------------------------------ | ------------------------------ |
73
+ | Boolean | `boolean` | `true` / `false` | Enable with defaults / disable |
74
+ | Value | `RuleConfigValue` | `"always"`, `["a","b"]`, `null` | Shorthand value |
75
+ | Object | `RuleConfig<T,O>` | `{severity, value, options, reason}` | Full configuration |
76
+
77
+ ```ts
78
+ type Rule<T, O> = RuleConfig<T, O> | Readonly<T> | boolean;
79
+
80
+ type RuleConfig<T, O> = {
81
+ severity?: Severity; // 'error' | 'warning' | 'info'
82
+ value?: Readonly<T>;
83
+ options?: Readonly<O>;
84
+ reason?: string;
85
+ };
86
+ ```
87
+
88
+ ### NodeRule / ChildNodeRule
89
+
90
+ - `NodeRule` -- Targets specific nodes by CSS selector, regex selector, ARIA roles, categories, or obsolete flag, then overrides their rule settings. Supports an optional `name` (must contain `/`) for named nodeRule expansion into virtual rules, and an optional `specConformance` (`'normative'` or `'non-normative'`) as metadata for downstream tools and reporting
91
+ - `ChildNodeRule` -- Similar to `NodeRule` but targets child nodes; includes an `inheritance` flag to control whether overrides propagate to descendants. Also supports `name` and `specConformance` (metadata) for named nodeRule expansion
92
+
93
+ ### Pretender Types
94
+
95
+ - `Pretender` -- Uses a CSS selector to make custom elements appear as standard elements for linting purposes; the `as` field specifies the element name or a detailed `OriginalNode`
96
+ - `OriginalNode` -- Defines an element's name, slots, namespace, attributes, inherited attributes, and ARIA properties
97
+ - `PretenderDetails` -- Normalized form `{data?, files?, imports?}` used after merging
98
+
99
+ ## Merge Algorithm
100
+
101
+ This is the core of the package. The `mergeConfig()` function combines two configurations with property-specific strategies.
102
+
103
+ ### mergeConfig() Overall Flow
104
+
105
+ ```ts
106
+ mergeConfig(a: Config, b?: Config): OptimizedConfig
107
+ ```
108
+
109
+ ```mermaid
110
+ flowchart TD
111
+ A["Input: base (a) + override (b)"] --> B{"b provided?"}
112
+ B -->|Yes| C["Set deleteExtendsProp = true"]
113
+ B -->|No| C2["Set b = {}, deleteExtendsProp = false"]
114
+ C --> D["Spread: {...a, ...b}\n(primitive fields: right-side wins)"]
115
+ C2 --> D
116
+ D --> E["Apply per-property\nmerge strategies\n(see table below)"]
117
+ E --> F{"deleteExtendsProp?"}
118
+ F -->|Yes| G["Delete extends from result"]
119
+ F -->|No| H["Keep extends"]
120
+ G --> I["deleteUndefProp()\nRemove undefined properties"]
121
+ H --> I
122
+ I --> J["Return OptimizedConfig"]
123
+ ```
124
+
125
+ ### Per-Property Merge Strategy Table
126
+
127
+ | Property | Strategy | Helper Function | Details |
128
+ | ---------------- | -------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- |
129
+ | `plugins` | Concat + deduplicate + normalize | `concatArray(uniquely=true, comparePropName='name')` | Same-name plugins have their settings shallow-merged |
130
+ | `parser` | Object shallow merge | `mergeObject()` | Right-side wins via `{...a, ...b}` |
131
+ | `parserOptions` | Object shallow merge | `mergeObject()` | Same as above |
132
+ | `specs` | Object shallow merge | `mergeObject()` | Same as above |
133
+ | `excludeFiles` | Concat + deduplicate | `concatArray(uniquely=true)` | Simple value deduplication |
134
+ | `severity` | Object shallow merge | `mergeObject()` | Same as parser |
135
+ | `pretenders` | Semantic merge | `mergePretenders()` | files/imports: override, data: append |
136
+ | `rules` | Per-rule merge | `mergeRules()` then `mergeRule()` | **Most complex -- see next section** |
137
+ | `nodeRules` | Concat + deduplicate by name | `concatArray(uniquely=true, comparePropName='name')` | Named entries deduplicated; unnamed entries appended |
138
+ | `childNodeRules` | Concat + deduplicate by name | `concatArray(uniquely=true, comparePropName='name')` | Same as nodeRules |
139
+ | `overrideMode` | Right-side wins | `b.overrideMode ?? a.overrideMode` | Simple precedence |
140
+ | `overrides` | Per-key recursive merge | `mergeOverrides()` | Calls `mergeConfig()` recursively for each key |
141
+ | `extends` | Concat then delete | `concatArray()` | Removed from result after merge |
142
+
143
+ ### mergeRule() -- Rule Merge Details
144
+
145
+ ```ts
146
+ mergeRule(a: Nullable<AnyRule>, b: AnyRule): AnyRule
147
+ ```
148
+
149
+ This function handles the most complex merge logic. Both inputs are first normalized via `optimizeRule()`.
150
+
151
+ ```mermaid
152
+ flowchart TD
153
+ Start["mergeRule(a, b)"] --> OptAB["Normalize both via optimizeRule()"]
154
+ OptAB --> ChkFalse{"b === false OR\nb.value === false?"}
155
+ ChkFalse -->|Yes| RetFalse["return false\n(absolute disable)"]
156
+ ChkFalse -->|No| ChkAUndef{"a === undefined?"}
157
+ ChkAUndef -->|Yes| RetB["return b"]
158
+ ChkAUndef -->|No| ChkBUndef{"b === undefined?"}
159
+ ChkBUndef -->|Yes| RetA["return a"]
160
+ ChkBUndef -->|No| ChkBVal{"b is Value?\n(primitive/null/array)"}
161
+ ChkBVal -->|Yes| ChkAVal{"a is Value?"}
162
+ ChkAVal -->|Yes| RetBVal["return b\n(right-side wins,\narrays override)"]
163
+ ChkAVal -->|No| MergeValObj["Keep a's severity/reason\nOverride value with b"]
164
+ ChkBVal -->|No| MergeObj["severity: b ?? a\nvalue: b ?? a\noptions: mergeObject(a, b)\nreason: b ?? a"]
165
+ ```
166
+
167
+ **Key Design Decisions:**
168
+
169
+ 1. **`false` is absolute disable** -- If the override is `false` (or `{value: false}`), the result is always `false`, regardless of what the base config says
170
+ 2. **Array values override** -- `["a","b"]` + `["c","d"]` results in `["c","d"]` (right-side wins), consistent with ESLint and Biome behavior
171
+ 3. **options uses shallow merge** -- While severity, value, and reason use right-side-wins precedence, options uses `mergeObject()` (shallow merge via `{...a, ...b}`)
172
+
173
+ ### Helper Functions
174
+
175
+ #### concatArray(a, b, uniquely?, comparePropName?)
176
+
177
+ Concatenates two arrays with optional deduplication:
178
+
179
+ - `uniquely=false` -- Simple concatenation, no deduplication
180
+ - `uniquely=true`, no `comparePropName` -- Exact-match deduplication
181
+ - `uniquely=true`, with `comparePropName` -- Deduplicates by the specified property name; when two objects share the same name, they are shallow-merged via object spread (e.g., plugin settings)
182
+ - Returns `undefined` for empty results
183
+
184
+ #### mergeObject(a, b)
185
+
186
+ Shallow merges two objects via `{...a, ...b}`. Right-side values take precedence at the top level. Removes undefined properties from the result.
187
+
188
+ #### mergeOverrides(a, b)
189
+
190
+ Collects the union of all keys from both override records. For each key, calls `mergeConfig(a[key], b[key])` recursively. Removes `$schema`, `extends`, and `overrides` from each result (since these are top-level-only properties).
191
+
192
+ #### mergePretenders(a, b)
193
+
194
+ Converts array-form pretenders to the normalized `PretenderDetails` form (`{data: [...]}`) then applies semantic merge: `files`/`imports` are overridden (right-side wins), `data` is appended (concatenated).
195
+
196
+ ## Template Rendering System
197
+
198
+ ### provideValue(template, data)
199
+
200
+ Renders a Mustache template string with the provided data:
201
+
202
+ - No variables in template -- Returns the template unchanged
203
+ - Variables present but no matching keys in data -- Returns `undefined`
204
+ - Variables present with matching keys -- Returns the rendered result
205
+
206
+ ### exchangeValueOnRule(rule, data)
207
+
208
+ Applies Mustache template rendering to all string values within a rule configuration:
209
+
210
+ - **value** -- String values are rendered; array elements are individually rendered
211
+ - **options** -- Recursively renders all string values in the options object
212
+ - **reason** -- Rendered as a string
213
+
214
+ This function is used by `nodeRules` and `childNodeRules` with `regexSelector`, where captured groups (`$0`, `$1`, named captures like `dataName`) are injected as template variables into rule settings.
215
+
216
+ ## Utility Functions
217
+
218
+ | Function | Purpose |
219
+ | --------------------- | ------------------------------------------------------------------------------------------------- |
220
+ | `cleanOptions()` | Extracts standard fields (`severity`, `value`, `options`, `reason`), removes undefined properties |
221
+ | `isRuleConfigValue()` | Type guard: returns `true` for primitives, `null`, and arrays (i.e., not a `RuleConfig` object) |
222
+ | `deleteUndefProp()` | Removes all properties with `undefined` values from a plain object in-place |
223
+
224
+ ## Key Source Files
225
+
226
+ | File | Purpose |
227
+ | --------------------- | ------------------------------------------------------------------------------------------------------- |
228
+ | `src/types.ts` | All type definitions (Config, Rule, Pretender, Violation, etc.) |
229
+ | `src/merge-config.ts` | `mergeConfig()`, `mergeRule()`, and all helper functions |
230
+ | `src/utils.ts` | `provideValue()`, `exchangeValueOnRule()`, `cleanOptions()`, `isRuleConfigValue()`, `deleteUndefProp()` |
231
+ | `src/index.ts` | Re-exports all public APIs |
232
+
233
+ ## External Dependencies
234
+
235
+ | Dependency | Purpose |
236
+ | ---------------------- | ---------------------------------------------- |
237
+ | `@markuplint/ml-ast` | `ParserOptions` type (type-only) |
238
+ | `@markuplint/selector` | `RegexSelector` type (re-exported) |
239
+ | `@markuplint/shared` | `Nullable` utility type |
240
+ | `is-plain-object` | Plain object detection in `deleteUndefProp()` |
241
+ | `mustache` | Template rendering engine for `provideValue()` |
242
+ | `type-fest` | `Writable` utility type |
243
+
244
+ ## Integration Points
245
+
246
+ ```mermaid
247
+ flowchart LR
248
+ subgraph upstream ["Upstream"]
249
+ fileResolver["@markuplint/file-resolver\n(reads config files,\nresolves extends chain)"]
250
+ end
251
+
252
+ subgraph pkg ["@markuplint/ml-config"]
253
+ mergeConfig["mergeConfig()\n(merge algorithm)"]
254
+ types["Config types"]
255
+ templates["Template rendering"]
256
+ end
257
+
258
+ subgraph downstream ["Downstream"]
259
+ mlCore["@markuplint/ml-core\n(applies rules using\nOptimizedConfig)"]
260
+ rules["@markuplint/rules\n(uses Rule<T,O>,\nRuleConfig<T,O> types)"]
261
+ end
262
+
263
+ fileResolver -->|"calls mergeConfig()\nfor each extends layer"| mergeConfig
264
+ mergeConfig -->|"produces OptimizedConfig"| mlCore
265
+ types -->|"Rule, RuleConfig types"| rules
266
+ ```
267
+
268
+ ### Upstream
269
+
270
+ - **`@markuplint/file-resolver`** -- Reads configuration files, resolves the extends chain, and calls `mergeConfig()` to combine layers
271
+
272
+ ### Downstream
273
+
274
+ - **`@markuplint/ml-core`** -- Receives the merged `OptimizedConfig` and applies rules to the parsed document
275
+ - **`@markuplint/rules`** -- Uses `Rule<T,O>` and `RuleConfig<T,O>` types to define rule implementations
276
+
277
+ ## Design Decisions
278
+
279
+ ### Why Not Flat Config?
280
+
281
+ ESLint's Flat Config approach was evaluated and rejected for markuplint:
282
+
283
+ - **ESLint itself re-added `extends` to Flat Config in March 2025** -- The pure flat approach proved insufficient even for JavaScript tooling
284
+ - **markuplint is JSON-based** -- Flat Config assumes JavaScript. HTML/markup developers (the primary audience) benefit from JSON's schema validation and language-agnostic editing
285
+ - **markuplint's `nodeRules`/`childNodeRules` are CSS-selector-based** -- These have no equivalent in Flat Config's file-pattern model
286
+ - **ESLint v9 migration caused significant community pain** -- The gradual transition required automated migration tools and years of ecosystem adaptation
287
+
288
+ **Conclusion:** Improving the JSON-based `extends` merge strategy is the optimal approach for markuplint.
289
+
290
+ ### Merge Strategy Principles
291
+
292
+ There is a logical distinction between how arrays are handled:
293
+
294
+ | Array Type | Examples | Merge Behavior | Rationale |
295
+ | ------------------------- | -------------------------------------- | -------------- | -------------------------------------- |
296
+ | **Top-level collections** | `plugins`, `excludeFiles`, `nodeRules` | Accumulate | Independent items forming a collection |
297
+ | **Rule values** | `["allowed-tag-1", "allowed-tag-2"]` | Override | A single rule's configuration value |
298
+
299
+ This aligns with ESLint and Biome, where rule values are always overridden by the more specific config.
300
+
301
+ ### Shallow Merge over Deep Merge
302
+
303
+ | Tool | Rule options merge | Rationale |
304
+ | ---------- | ---------------------------- | --------------------------------------------------------------------- |
305
+ | ESLint | Complete replacement | Simplest, but can be surprising |
306
+ | Biome | Deep merge (via Merge trait) | Full flexibility, higher complexity |
307
+ | markuplint | **Shallow merge** | Middle ground: top-level keys are merged, nested objects are replaced |
308
+
309
+ The `deepmerge` library was removed in favor of simple object spread (`{...a, ...b}`). This is sufficient because all merged objects in markuplint config (parser, specs, parserOptions, severity, plugin settings, rule options) are flat key-value maps.
310
+
311
+ ## Documentation Map
312
+
313
+ - [Migration Guide](../../docs/migration/v4-v5/config.md) -- Breaking changes between major versions
314
+ - [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting