@markuplint/ml-config 4.8.13 → 4.8.15

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,282 @@
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 フラグで対象ノードを限定し、ルール設定をオーバーライド
91
+ - `ChildNodeRule` -- `NodeRule` と類似だが子ノードを対象とする。`inheritance` フラグで子孫への継承を制御
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 を deep merge |
130
+ | `parser` | オブジェクト deep merge | `mergeObject()` | 右辺優先、deepmerge ライブラリ使用 |
131
+ | `parserOptions` | オブジェクト deep merge | `mergeObject()` | 同上 |
132
+ | `specs` | オブジェクト deep merge | `mergeObject()` | 同上 |
133
+ | `excludeFiles` | 結合+重複排除 | `concatArray(uniquely=true)` | 単純な値の重複排除 |
134
+ | `severity` | オブジェクト deep merge | `mergeObject()` | parser と同様 |
135
+ | `pretenders` | 形式変換+deep merge | `mergePretenders()` | 配列を PretenderDetails に変換後にマージ |
136
+ | `rules` | ルール別マージ | `mergeRules()` → `mergeRule()` | **最も複雑 -- 次節で詳述** |
137
+ | `nodeRules` | 結合(重複排除なし) | `concatArray()` | 両配列を単純連結 |
138
+ | `childNodeRules` | 結合(重複排除なし) | `concatArray()` | 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()` で正規化されます(非推奨の `option` から `options` への移行を含む)。
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| ChkBothArr{"両方とも配列?"}
163
+ ChkBothArr -->|Yes| RetConcat["return [...a, ...b]\n(連結)"]
164
+ ChkBothArr -->|No| RetBVal["return b\n(右辺優先)"]
165
+ ChkAVal -->|No| MergeValObj["a の severity/reason を保持\nvalue を置換\n(配列は連結)"]
166
+ ChkBVal -->|No| MergeObj["severity: b ?? a\nvalue: b ?? a\noptions: mergeObject(a, b)\nreason: b ?? a"]
167
+ ```
168
+
169
+ **重要な設計判断:**
170
+
171
+ 1. **`false` は絶対無効化** -- override が `false`(または `{value: false}`)なら、base が何であっても結果は常に `false`
172
+ 2. **配列値は連結** -- `["a","b"]` + `["c","d"]` は `["a","b","c","d"]` になり、extends チェーンでルールを段階的に追加可能
173
+ 3. **options は deep merge** -- severity、value、reason は右辺優先だが、options のみ `mergeObject()`(deepmerge ライブラリによる deep merge)を使用
174
+
175
+ ### ヘルパー関数
176
+
177
+ #### concatArray(a, b, uniquely?, comparePropName?)
178
+
179
+ オプションの重複排除付きで2つの配列を連結:
180
+
181
+ - `uniquely=false` -- 単純な連結、重複排除なし
182
+ - `uniquely=true`、`comparePropName` なし -- 完全一致で重複排除
183
+ - `uniquely=true`、`comparePropName` あり -- 指定プロパティ名で重複排除。同名オブジェクトは `mergeObject()` でマージ(例: プラグインの settings)
184
+ - 空の結果には `undefined` を返す
185
+
186
+ #### mergeObject(a, b)
187
+
188
+ `deepmerge` ライブラリを使用した再帰的な deep merge。右辺の値が優先。結果から undefined プロパティを除去。
189
+
190
+ #### mergeOverrides(a, b)
191
+
192
+ 両方の override レコードから全キーの集合を取得。各キーに対して `mergeConfig(a[key], b[key])` を再帰呼び出し。各結果から `$schema`、`extends`、`overrides` を削除(これらはトップレベル専用プロパティ)。
193
+
194
+ #### mergePretenders(a, b)
195
+
196
+ 配列形式の pretenders を正規化形式 `PretenderDetails`(`{data: [...]}`)に変換してから `mergeObject()` で deep merge。
197
+
198
+ ## テンプレートレンダリングシステム
199
+
200
+ ### provideValue(template, data)
201
+
202
+ Mustache テンプレート文字列を提供されたデータでレンダリング:
203
+
204
+ - テンプレートに変数がない -- テンプレートをそのまま返す
205
+ - 変数があるが data にマッチするキーがない -- `undefined` を返す
206
+ - 変数があり data にマッチするキーがある -- レンダリング結果を返す
207
+
208
+ ### exchangeValueOnRule(rule, data)
209
+
210
+ ルール設定内のすべての文字列値に Mustache テンプレートレンダリングを適用:
211
+
212
+ - **value** -- 文字列値はレンダリングされる。配列の各要素も個別にレンダリング
213
+ - **options** -- options オブジェクト内のすべての文字列値を再帰的にレンダリング
214
+ - **reason** -- 文字列としてレンダリング
215
+
216
+ この関数は `nodeRules` / `childNodeRules` の `regexSelector` で使用され、キャプチャグループ(`$0`、`$1`、`dataName` 等の名前付きキャプチャ)がテンプレート変数としてルール設定に注入されます。
217
+
218
+ ## ユーティリティ関数
219
+
220
+ | 関数 | 目的 |
221
+ | --------------------- | -------------------------------------------------------------------------------------------------------- |
222
+ | `cleanOptions()` | 非推奨の `option` フィールドを `options` に正規化し、標準フィールドを抽出して undefined プロパティを除去 |
223
+ | `isRuleConfigValue()` | 型ガード: プリミティブ、`null`、配列(= `RuleConfig` オブジェクトではない)に対して `true` を返す |
224
+ | `deleteUndefProp()` | プレーンオブジェクトから `undefined` 値のプロパティをすべて in-place で削除 |
225
+
226
+ ## 主要ソースファイル
227
+
228
+ | ファイル | 目的 |
229
+ | --------------------- | ------------------------------------------------------------------------------------------------------- |
230
+ | `src/types.ts` | 全型定義(Config, Rule, Pretender, Violation 等) |
231
+ | `src/merge-config.ts` | `mergeConfig()`、`mergeRule()`、全ヘルパー関数 |
232
+ | `src/utils.ts` | `provideValue()`、`exchangeValueOnRule()`、`cleanOptions()`、`isRuleConfigValue()`、`deleteUndefProp()` |
233
+ | `src/index.ts` | 全公開 API の再エクスポート |
234
+
235
+ ## 外部依存関係
236
+
237
+ | 依存パッケージ | 用途 |
238
+ | ---------------------- | --------------------------------------------------- |
239
+ | `@markuplint/ml-ast` | `ParserOptions` 型(型のみ) |
240
+ | `@markuplint/selector` | `RegexSelector` 型(再エクスポート) |
241
+ | `@markuplint/shared` | `Nullable` ユーティリティ型 |
242
+ | `deepmerge` | `mergeObject()` の deep merge 実装 |
243
+ | `is-plain-object` | `deleteUndefProp()` でのプレーンオブジェクト判定 |
244
+ | `mustache` | `provideValue()` のテンプレートレンダリングエンジン |
245
+ | `type-fest` | `Writable` ユーティリティ型 |
246
+
247
+ ## 統合ポイント
248
+
249
+ ```mermaid
250
+ flowchart LR
251
+ subgraph upstream ["上流"]
252
+ fileResolver["@markuplint/file-resolver\n(設定ファイル読み込み,\nextends チェーン解決)"]
253
+ end
254
+
255
+ subgraph pkg ["@markuplint/ml-config"]
256
+ mergeConfig["mergeConfig()\n(マージアルゴリズム)"]
257
+ types["Config 型定義"]
258
+ templates["テンプレートレンダリング"]
259
+ end
260
+
261
+ subgraph downstream ["下流"]
262
+ mlCore["@markuplint/ml-core\n(OptimizedConfig を使用して\nルールを適用)"]
263
+ rules["@markuplint/rules\n(Rule<T,O>,\nRuleConfig<T,O> 型を使用)"]
264
+ end
265
+
266
+ fileResolver -->|"各 extends レイヤーで\nmergeConfig() を呼び出し"| mergeConfig
267
+ mergeConfig -->|"OptimizedConfig を生成"| mlCore
268
+ types -->|"Rule, RuleConfig 型"| rules
269
+ ```
270
+
271
+ ### 上流
272
+
273
+ - **`@markuplint/file-resolver`** -- 設定ファイルを読み込み、extends チェーンを解決し、`mergeConfig()` を呼び出してレイヤーを統合
274
+
275
+ ### 下流
276
+
277
+ - **`@markuplint/ml-core`** -- マージ済みの `OptimizedConfig` を受け取り、パース済みドキュメントにルールを適用
278
+ - **`@markuplint/rules`** -- `Rule<T,O>` と `RuleConfig<T,O>` 型を使用してルール実装を定義
279
+
280
+ ## ドキュメントマップ
281
+
282
+ - [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング
@@ -0,0 +1,282 @@
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
91
+ - `ChildNodeRule` -- Similar to `NodeRule` but targets child nodes; includes an `inheritance` flag to control whether overrides propagate to descendants
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 deep-merged |
130
+ | `parser` | Object deep merge | `mergeObject()` | Right-side wins, uses deepmerge library |
131
+ | `parserOptions` | Object deep merge | `mergeObject()` | Same as above |
132
+ | `specs` | Object deep merge | `mergeObject()` | Same as above |
133
+ | `excludeFiles` | Concat + deduplicate | `concatArray(uniquely=true)` | Simple value deduplication |
134
+ | `severity` | Object deep merge | `mergeObject()` | Same as parser |
135
+ | `pretenders` | Format conversion + deep merge | `mergePretenders()` | Array converted to PretenderDetails, then merged |
136
+ | `rules` | Per-rule merge | `mergeRules()` then `mergeRule()` | **Most complex -- see next section** |
137
+ | `nodeRules` | Concat (no deduplicate) | `concatArray()` | Both arrays simply concatenated |
138
+ | `childNodeRules` | Concat (no deduplicate) | `concatArray()` | 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()` (which handles the deprecated `option` to `options` migration).
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| ChkBothArr{"Both arrays?"}
163
+ ChkBothArr -->|Yes| RetConcat["return [...a, ...b]\n(concatenate)"]
164
+ ChkBothArr -->|No| RetBVal["return b\n(right-side wins)"]
165
+ ChkAVal -->|No| MergeValObj["Keep a's severity/reason\nReplace value\n(arrays concatenated)"]
166
+ ChkBVal -->|No| MergeObj["severity: b ?? a\nvalue: b ?? a\noptions: mergeObject(a, b)\nreason: b ?? a"]
167
+ ```
168
+
169
+ **Key Design Decisions:**
170
+
171
+ 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
172
+ 2. **Array values are concatenated** -- `["a","b"]` + `["c","d"]` results in `["a","b","c","d"]`, enabling incremental rule additions across extends chains
173
+ 3. **options uses deep merge** -- While severity, value, and reason use right-side-wins precedence, options alone uses `mergeObject()` (deep merge via deepmerge library)
174
+
175
+ ### Helper Functions
176
+
177
+ #### concatArray(a, b, uniquely?, comparePropName?)
178
+
179
+ Concatenates two arrays with optional deduplication:
180
+
181
+ - `uniquely=false` -- Simple concatenation, no deduplication
182
+ - `uniquely=true`, no `comparePropName` -- Exact-match deduplication
183
+ - `uniquely=true`, with `comparePropName` -- Deduplicates by the specified property name; when two objects share the same name, they are merged via `mergeObject()` (e.g., plugin settings)
184
+ - Returns `undefined` for empty results
185
+
186
+ #### mergeObject(a, b)
187
+
188
+ Deep merges two objects using the `deepmerge` library. Right-side values take precedence. Removes undefined properties from the result.
189
+
190
+ #### mergeOverrides(a, b)
191
+
192
+ 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).
193
+
194
+ #### mergePretenders(a, b)
195
+
196
+ Converts array-form pretenders to the normalized `PretenderDetails` form (`{data: [...]}`) before deep merging with `mergeObject()`.
197
+
198
+ ## Template Rendering System
199
+
200
+ ### provideValue(template, data)
201
+
202
+ Renders a Mustache template string with the provided data:
203
+
204
+ - No variables in template -- Returns the template unchanged
205
+ - Variables present but no matching keys in data -- Returns `undefined`
206
+ - Variables present with matching keys -- Returns the rendered result
207
+
208
+ ### exchangeValueOnRule(rule, data)
209
+
210
+ Applies Mustache template rendering to all string values within a rule configuration:
211
+
212
+ - **value** -- String values are rendered; array elements are individually rendered
213
+ - **options** -- Recursively renders all string values in the options object
214
+ - **reason** -- Rendered as a string
215
+
216
+ 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.
217
+
218
+ ## Utility Functions
219
+
220
+ | Function | Purpose |
221
+ | --------------------- | --------------------------------------------------------------------------------------------------------- |
222
+ | `cleanOptions()` | Normalizes deprecated `option` field to `options`, extracts standard fields, removes undefined properties |
223
+ | `isRuleConfigValue()` | Type guard: returns `true` for primitives, `null`, and arrays (i.e., not a `RuleConfig` object) |
224
+ | `deleteUndefProp()` | Removes all properties with `undefined` values from a plain object in-place |
225
+
226
+ ## Key Source Files
227
+
228
+ | File | Purpose |
229
+ | --------------------- | ------------------------------------------------------------------------------------------------------- |
230
+ | `src/types.ts` | All type definitions (Config, Rule, Pretender, Violation, etc.) |
231
+ | `src/merge-config.ts` | `mergeConfig()`, `mergeRule()`, and all helper functions |
232
+ | `src/utils.ts` | `provideValue()`, `exchangeValueOnRule()`, `cleanOptions()`, `isRuleConfigValue()`, `deleteUndefProp()` |
233
+ | `src/index.ts` | Re-exports all public APIs |
234
+
235
+ ## External Dependencies
236
+
237
+ | Dependency | Purpose |
238
+ | ---------------------- | ------------------------------------------------- |
239
+ | `@markuplint/ml-ast` | `ParserOptions` type (type-only) |
240
+ | `@markuplint/selector` | `RegexSelector` type (re-exported) |
241
+ | `@markuplint/shared` | `Nullable` utility type |
242
+ | `deepmerge` | Deep merge implementation used by `mergeObject()` |
243
+ | `is-plain-object` | Plain object detection in `deleteUndefProp()` |
244
+ | `mustache` | Template rendering engine for `provideValue()` |
245
+ | `type-fest` | `Writable` utility type |
246
+
247
+ ## Integration Points
248
+
249
+ ```mermaid
250
+ flowchart LR
251
+ subgraph upstream ["Upstream"]
252
+ fileResolver["@markuplint/file-resolver\n(reads config files,\nresolves extends chain)"]
253
+ end
254
+
255
+ subgraph pkg ["@markuplint/ml-config"]
256
+ mergeConfig["mergeConfig()\n(merge algorithm)"]
257
+ types["Config types"]
258
+ templates["Template rendering"]
259
+ end
260
+
261
+ subgraph downstream ["Downstream"]
262
+ mlCore["@markuplint/ml-core\n(applies rules using\nOptimizedConfig)"]
263
+ rules["@markuplint/rules\n(uses Rule<T,O>,\nRuleConfig<T,O> types)"]
264
+ end
265
+
266
+ fileResolver -->|"calls mergeConfig()\nfor each extends layer"| mergeConfig
267
+ mergeConfig -->|"produces OptimizedConfig"| mlCore
268
+ types -->|"Rule, RuleConfig types"| rules
269
+ ```
270
+
271
+ ### Upstream
272
+
273
+ - **`@markuplint/file-resolver`** -- Reads configuration files, resolves the extends chain, and calls `mergeConfig()` to combine layers
274
+
275
+ ### Downstream
276
+
277
+ - **`@markuplint/ml-core`** -- Receives the merged `OptimizedConfig` and applies rules to the parsed document
278
+ - **`@markuplint/rules`** -- Uses `Rule<T,O>` and `RuleConfig<T,O>` types to define rule implementations
279
+
280
+ ## Documentation Map
281
+
282
+ - [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
package/CHANGELOG.md CHANGED
@@ -3,13 +3,17 @@
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
- ## [4.8.13](https://github.com/markuplint/markuplint/compare/@markuplint/ml-config@4.8.12...@markuplint/ml-config@4.8.13) (2025-08-24)
6
+ ## [4.8.15](https://github.com/markuplint/markuplint/compare/@markuplint/ml-config@4.8.14...@markuplint/ml-config@4.8.15) (2026-02-10)
7
7
 
8
8
  **Note:** Version bump only for package @markuplint/ml-config
9
9
 
10
+ ## [4.8.14](https://github.com/markuplint/markuplint/compare/@markuplint/ml-config@4.8.13...@markuplint/ml-config@4.8.14) (2025-11-05)
10
11
 
12
+ **Note:** Version bump only for package @markuplint/ml-config
11
13
 
14
+ ## [4.8.13](https://github.com/markuplint/markuplint/compare/@markuplint/ml-config@4.8.12...@markuplint/ml-config@4.8.13) (2025-08-24)
12
15
 
16
+ **Note:** Version bump only for package @markuplint/ml-config
13
17
 
14
18
  ## [4.8.12](https://github.com/markuplint/markuplint/compare/@markuplint/ml-config@4.8.11...@markuplint/ml-config@4.8.12) (2025-08-13)
15
19