1use std::sync::LazyLock;
2
3use crate::rule::Rule;
4
5use super::flavor::normalize_key;
6
7static DEFAULT_REGISTRY: LazyLock<RuleRegistry> = LazyLock::new(|| {
13 let default_config = super::types::Config::default();
14 let rules = crate::rules::all_rules(&default_config);
15 RuleRegistry::from_rules(&rules)
16});
17
18pub fn default_registry() -> &'static RuleRegistry {
24 &DEFAULT_REGISTRY
25}
26
27pub struct RuleRegistry {
29 pub rule_schemas: std::collections::BTreeMap<String, toml::map::Map<String, toml::Value>>,
31 pub rule_aliases: std::collections::BTreeMap<String, std::collections::HashMap<String, String>>,
33}
34
35impl RuleRegistry {
36 pub fn from_rules(rules: &[Box<dyn Rule>]) -> Self {
38 let mut rule_schemas = std::collections::BTreeMap::new();
39 let mut rule_aliases = std::collections::BTreeMap::new();
40
41 for rule in rules {
42 let norm_name = if let Some((name, toml::Value::Table(table))) = rule.default_config_section() {
43 let norm_name = normalize_key(&name); rule_schemas.insert(norm_name.clone(), table);
45 norm_name
46 } else {
47 let norm_name = normalize_key(rule.name()); rule_schemas.insert(norm_name.clone(), toml::map::Map::new());
49 norm_name
50 };
51
52 if let Some(aliases) = rule.config_aliases() {
54 rule_aliases.insert(norm_name, aliases);
55 }
56 }
57
58 RuleRegistry {
59 rule_schemas,
60 rule_aliases,
61 }
62 }
63
64 pub fn rule_names(&self) -> std::collections::BTreeSet<String> {
66 self.rule_schemas.keys().cloned().collect()
67 }
68
69 pub fn config_keys_for(&self, rule: &str) -> Option<std::collections::BTreeSet<String>> {
71 self.rule_schemas.get(rule).map(|schema| {
72 let mut all_keys = std::collections::BTreeSet::new();
73
74 all_keys.insert("severity".to_string());
76
77 for key in schema.keys() {
79 all_keys.insert(key.clone());
80 }
81
82 for key in schema.keys() {
84 all_keys.insert(key.replace('_', "-"));
86 all_keys.insert(key.replace('-', "_"));
88 all_keys.insert(normalize_key(key));
90 }
91
92 if let Some(aliases) = self.rule_aliases.get(rule) {
94 for alias_key in aliases.keys() {
95 all_keys.insert(alias_key.clone());
96 all_keys.insert(alias_key.replace('_', "-"));
98 all_keys.insert(alias_key.replace('-', "_"));
99 all_keys.insert(normalize_key(alias_key));
100 }
101 }
102
103 all_keys
104 })
105 }
106
107 pub fn expected_value_for(&self, rule: &str, key: &str) -> Option<&toml::Value> {
111 let schema = self.rule_schemas.get(rule)?;
112
113 if let Some(aliases) = self.rule_aliases.get(rule)
115 && let Some(canonical_key) = aliases.get(key)
116 && let Some(value) = schema.get(canonical_key)
117 {
118 return filter_nullable_sentinel(value);
119 }
120
121 if let Some(value) = schema.get(key) {
123 return filter_nullable_sentinel(value);
124 }
125
126 let key_variants = [
128 key.replace('-', "_"), key.replace('_', "-"), normalize_key(key), ];
132
133 for variant in &key_variants {
134 if let Some(value) = schema.get(variant) {
135 return filter_nullable_sentinel(value);
136 }
137 }
138
139 None
140 }
141
142 pub fn resolve_rule_name(&self, name: &str) -> Option<String> {
149 let normalized = normalize_key(name);
151 if self.rule_schemas.contains_key(&normalized) {
152 return Some(normalized);
153 }
154
155 resolve_rule_name_alias(name).map(|s| s.to_string())
157 }
158}
159
160fn filter_nullable_sentinel(value: &toml::Value) -> Option<&toml::Value> {
163 if crate::rule_config_serde::is_nullable_sentinel(value) {
164 None
165 } else {
166 Some(value)
167 }
168}
169
170pub static RULE_ALIAS_MAP: phf::Map<&'static str, &'static str> = phf::phf_map! {
173 "MD001" => "MD001",
175 "MD003" => "MD003",
176 "MD004" => "MD004",
177 "MD005" => "MD005",
178 "MD007" => "MD007",
179 "MD009" => "MD009",
180 "MD010" => "MD010",
181 "MD011" => "MD011",
182 "MD012" => "MD012",
183 "MD013" => "MD013",
184 "MD014" => "MD014",
185 "MD018" => "MD018",
186 "MD019" => "MD019",
187 "MD020" => "MD020",
188 "MD021" => "MD021",
189 "MD022" => "MD022",
190 "MD023" => "MD023",
191 "MD024" => "MD024",
192 "MD025" => "MD025",
193 "MD026" => "MD026",
194 "MD027" => "MD027",
195 "MD028" => "MD028",
196 "MD029" => "MD029",
197 "MD030" => "MD030",
198 "MD031" => "MD031",
199 "MD032" => "MD032",
200 "MD033" => "MD033",
201 "MD034" => "MD034",
202 "MD035" => "MD035",
203 "MD036" => "MD036",
204 "MD037" => "MD037",
205 "MD038" => "MD038",
206 "MD039" => "MD039",
207 "MD040" => "MD040",
208 "MD041" => "MD041",
209 "MD042" => "MD042",
210 "MD043" => "MD043",
211 "MD044" => "MD044",
212 "MD045" => "MD045",
213 "MD046" => "MD046",
214 "MD047" => "MD047",
215 "MD048" => "MD048",
216 "MD049" => "MD049",
217 "MD050" => "MD050",
218 "MD051" => "MD051",
219 "MD052" => "MD052",
220 "MD053" => "MD053",
221 "MD054" => "MD054",
222 "MD055" => "MD055",
223 "MD056" => "MD056",
224 "MD057" => "MD057",
225 "MD058" => "MD058",
226 "MD059" => "MD059",
227 "MD060" => "MD060",
228 "MD061" => "MD061",
229 "MD062" => "MD062",
230 "MD063" => "MD063",
231 "MD064" => "MD064",
232 "MD065" => "MD065",
233 "MD066" => "MD066",
234 "MD067" => "MD067",
235 "MD068" => "MD068",
236 "MD069" => "MD069",
237 "MD070" => "MD070",
238 "MD071" => "MD071",
239 "MD072" => "MD072",
240 "MD073" => "MD073",
241 "MD074" => "MD074",
242 "MD075" => "MD075",
243 "MD076" => "MD076",
244 "MD077" => "MD077",
245
246 "HEADING-INCREMENT" => "MD001",
248 "HEADING-STYLE" => "MD003",
249 "UL-STYLE" => "MD004",
250 "LIST-INDENT" => "MD005",
251 "UL-INDENT" => "MD007",
252 "NO-TRAILING-SPACES" => "MD009",
253 "NO-HARD-TABS" => "MD010",
254 "NO-REVERSED-LINKS" => "MD011",
255 "NO-MULTIPLE-BLANKS" => "MD012",
256 "LINE-LENGTH" => "MD013",
257 "COMMANDS-SHOW-OUTPUT" => "MD014",
258 "NO-MISSING-SPACE-ATX" => "MD018",
259 "NO-MULTIPLE-SPACE-ATX" => "MD019",
260 "NO-MISSING-SPACE-CLOSED-ATX" => "MD020",
261 "NO-MULTIPLE-SPACE-CLOSED-ATX" => "MD021",
262 "BLANKS-AROUND-HEADINGS" => "MD022",
263 "HEADING-START-LEFT" => "MD023",
264 "NO-DUPLICATE-HEADING" => "MD024",
265 "SINGLE-TITLE" => "MD025",
266 "SINGLE-H1" => "MD025",
267 "NO-TRAILING-PUNCTUATION" => "MD026",
268 "NO-MULTIPLE-SPACE-BLOCKQUOTE" => "MD027",
269 "NO-BLANKS-BLOCKQUOTE" => "MD028",
270 "OL-PREFIX" => "MD029",
271 "LIST-MARKER-SPACE" => "MD030",
272 "BLANKS-AROUND-FENCES" => "MD031",
273 "BLANKS-AROUND-LISTS" => "MD032",
274 "NO-INLINE-HTML" => "MD033",
275 "NO-BARE-URLS" => "MD034",
276 "HR-STYLE" => "MD035",
277 "NO-EMPHASIS-AS-HEADING" => "MD036",
278 "NO-SPACE-IN-EMPHASIS" => "MD037",
279 "NO-SPACE-IN-CODE" => "MD038",
280 "NO-SPACE-IN-LINKS" => "MD039",
281 "FENCED-CODE-LANGUAGE" => "MD040",
282 "FIRST-LINE-HEADING" => "MD041",
283 "FIRST-LINE-H1" => "MD041",
284 "NO-EMPTY-LINKS" => "MD042",
285 "REQUIRED-HEADINGS" => "MD043",
286 "PROPER-NAMES" => "MD044",
287 "NO-ALT-TEXT" => "MD045",
288 "CODE-BLOCK-STYLE" => "MD046",
289 "SINGLE-TRAILING-NEWLINE" => "MD047",
290 "CODE-FENCE-STYLE" => "MD048",
291 "EMPHASIS-STYLE" => "MD049",
292 "STRONG-STYLE" => "MD050",
293 "LINK-FRAGMENTS" => "MD051",
294 "REFERENCE-LINKS-IMAGES" => "MD052",
295 "LINK-IMAGE-REFERENCE-DEFINITIONS" => "MD053",
296 "LINK-IMAGE-STYLE" => "MD054",
297 "TABLE-PIPE-STYLE" => "MD055",
298 "TABLE-COLUMN-COUNT" => "MD056",
299 "EXISTING-RELATIVE-LINKS" => "MD057",
300 "BLANKS-AROUND-TABLES" => "MD058",
301 "DESCRIPTIVE-LINK-TEXT" => "MD059",
302 "TABLE-CELL-ALIGNMENT" => "MD060",
303 "TABLE-FORMAT" => "MD060",
304 "FORBIDDEN-TERMS" => "MD061",
305 "LINK-DESTINATION-WHITESPACE" => "MD062",
306 "HEADING-CAPITALIZATION" => "MD063",
307 "NO-MULTIPLE-CONSECUTIVE-SPACES" => "MD064",
308 "BLANKS-AROUND-HORIZONTAL-RULES" => "MD065",
309 "FOOTNOTE-VALIDATION" => "MD066",
310 "FOOTNOTE-DEFINITION-ORDER" => "MD067",
311 "EMPTY-FOOTNOTE-DEFINITION" => "MD068",
312 "NO-DUPLICATE-LIST-MARKERS" => "MD069",
313 "NESTED-CODE-FENCE" => "MD070",
314 "BLANK-LINE-AFTER-FRONTMATTER" => "MD071",
315 "FRONTMATTER-KEY-SORT" => "MD072",
316 "TOC-VALIDATION" => "MD073",
317 "MKDOCS-NAV" => "MD074",
318 "ORPHANED-TABLE-ROWS" => "MD075",
319 "LIST-ITEM-SPACING" => "MD076",
320 "LIST-CONTINUATION-INDENT" => "MD077",
321};
322
323pub fn resolve_rule_name_alias(key: &str) -> Option<&'static str> {
327 let normalized_key = key.to_ascii_uppercase().replace('_', "-");
329
330 RULE_ALIAS_MAP.get(normalized_key.as_str()).copied()
332}
333
334pub fn resolve_rule_name(name: &str) -> String {
342 resolve_rule_name_alias(name)
343 .map(|s| s.to_string())
344 .unwrap_or_else(|| normalize_key(name))
345}
346
347pub fn resolve_rule_names(input: &str) -> std::collections::HashSet<String> {
351 input
352 .split(',')
353 .map(|s| s.trim())
354 .filter(|s| !s.is_empty())
355 .map(resolve_rule_name)
356 .collect()
357}
358
359pub fn is_valid_rule_name(name: &str) -> bool {
363 if name.eq_ignore_ascii_case("all") {
365 return true;
366 }
367 resolve_rule_name_alias(name).is_some()
368}